为什么我自己写在NASM上的memcpy不能复制超过340000000字节?
Why my own memcpy written on NASM can not copy more than 340000000 bytes?
我正在学习nasm。我写了一个简单的函数,将内存从源复制到目标。我在 C 中测试。
section .text
global _myMemcpy
_myMemcpy:
mov eax, [esp + 4]
mov ecx, [esp + 8]
add [esp + 12], eax
lp:
mov dl, [ecx]
mov [eax], dl
inc eax
inc ecx
cmp eax, [esp + 12]
jl lp
endlp:
mov eax, [esp + 4]
ret
C 程序:
#include <string.h>
#define Times 340000000
extern void* _myMemcpy(void* dest, void* src, size_t size);
char sr[Times];
char ds[Times];
int main(void)
{
memset(sr, 'a', Times);
_myMemcpy(ds, sr, Times);
return 0;
}
我目前正在使用 Ubuntu OS。当我编译和 link 这两个带有 $ nasm -f elf m.asm && gcc -Wall -m32 m.o p.c && ./a.out
的文件时,当 Times
的值小于 340000000 时它工作正常。当它更大时,_myMemcpy
只复制第一个字节的来源到目的地。我不知道问题出在哪里。每个建议都会有用。
您正在对指针进行有符号比较;不要那样做。在这种情况下使用 jne
,因为您将始终在退出点达到完全相等。
或者,如果您想要与指针进行关系比较,通常 jb
和 jae
等无符号条件最有意义。 (将虚拟地址 space 视为最低地址为 0 的平面线性 4GiB 是正常的,因此您需要在该范围的中间递增才能工作)。
对于大于 ~300MiB 大小的数组,以及 PIE 可执行文件的默认链接描述文件,显然其中之一将跨越有符号正数和有符号负数之间的 2GiB 边界1.因此,如果您将其视为有符号整数,则您计算的结束指针将为“负”。 (与 x86-64 不同,其中跨越虚拟地址中间的非规范“孔”-space 意味着数组永远不能跨越有符号环绕边界: - 有时它 在那里使用带符号的比较是否有意义。)
如果您单步执行并查看指针值,以及您使用 size += dest
(add [esp + 12], eax
) 创建的内存值,您应该会在调试器中看到这一点。作为带符号的操作,它会溢出以创建一个负数 end_pointer,而起始指针仍为正数。 pos < neg
在第一次迭代时为 false,因此您的循环退出,您可以在单步执行时看到这一点。
脚注 1:在我的系统上,在 GDB(禁用 ASLR)下,在 start
之后将可执行文件映射到 Linux 的默认值PIE 的基地址(进入地址低半部分的 2/3 space,即 0x5555...),我用你的测试用例检查了地址:
sr
在 0x56559040
ds
在 0x6a998d40
ds
结束于 p /x sizeof(ds) + ds
= 0x7edd8a40
所以如果它大得多,它会穿过 0x80000000
。这就是为什么 340000000
避免了你的错误,但更大的尺寸揭示了它。
顺便说一句,在 32 位内核下,Linux 默认为内核和用户 space 之间地址 space 的 3:1 分割,所以即使在那里有可能发生这种情况。但是在 64 位内核下,32 位进程可以拥有整个 4 GiB 地址 space。 (内核保留的一两页除外:另请参阅 。这也意味着像您正在做的那样形成指向任何数组的尾数的指针(ISO C 承诺是有效的) ), 不会回绕并且仍然会在指向对象的指针上方进行比较。)
这不会在 64 位模式下发生:有足够的地址 space 可以在用户和内核之间平均分配它,并且在高范围和低范围之间有一个巨大的非规范孔.
我正在学习nasm。我写了一个简单的函数,将内存从源复制到目标。我在 C 中测试。
section .text
global _myMemcpy
_myMemcpy:
mov eax, [esp + 4]
mov ecx, [esp + 8]
add [esp + 12], eax
lp:
mov dl, [ecx]
mov [eax], dl
inc eax
inc ecx
cmp eax, [esp + 12]
jl lp
endlp:
mov eax, [esp + 4]
ret
C 程序:
#include <string.h>
#define Times 340000000
extern void* _myMemcpy(void* dest, void* src, size_t size);
char sr[Times];
char ds[Times];
int main(void)
{
memset(sr, 'a', Times);
_myMemcpy(ds, sr, Times);
return 0;
}
我目前正在使用 Ubuntu OS。当我编译和 link 这两个带有 $ nasm -f elf m.asm && gcc -Wall -m32 m.o p.c && ./a.out
的文件时,当 Times
的值小于 340000000 时它工作正常。当它更大时,_myMemcpy
只复制第一个字节的来源到目的地。我不知道问题出在哪里。每个建议都会有用。
您正在对指针进行有符号比较;不要那样做。在这种情况下使用 jne
,因为您将始终在退出点达到完全相等。
或者,如果您想要与指针进行关系比较,通常 jb
和 jae
等无符号条件最有意义。 (将虚拟地址 space 视为最低地址为 0 的平面线性 4GiB 是正常的,因此您需要在该范围的中间递增才能工作)。
对于大于 ~300MiB 大小的数组,以及 PIE 可执行文件的默认链接描述文件,显然其中之一将跨越有符号正数和有符号负数之间的 2GiB 边界1.因此,如果您将其视为有符号整数,则您计算的结束指针将为“负”。 (与 x86-64 不同,其中跨越虚拟地址中间的非规范“孔”-space 意味着数组永远不能跨越有符号环绕边界:
如果您单步执行并查看指针值,以及您使用 size += dest
(add [esp + 12], eax
) 创建的内存值,您应该会在调试器中看到这一点。作为带符号的操作,它会溢出以创建一个负数 end_pointer,而起始指针仍为正数。 pos < neg
在第一次迭代时为 false,因此您的循环退出,您可以在单步执行时看到这一点。
脚注 1:在我的系统上,在 GDB(禁用 ASLR)下,在 start
之后将可执行文件映射到 Linux 的默认值PIE 的基地址(进入地址低半部分的 2/3 space,即 0x5555...),我用你的测试用例检查了地址:
sr
在0x56559040
ds
在0x6a998d40
ds
结束于p /x sizeof(ds) + ds
=0x7edd8a40
所以如果它大得多,它会穿过 0x80000000
。这就是为什么 340000000
避免了你的错误,但更大的尺寸揭示了它。
顺便说一句,在 32 位内核下,Linux 默认为内核和用户 space 之间地址 space 的 3:1 分割,所以即使在那里有可能发生这种情况。但是在 64 位内核下,32 位进程可以拥有整个 4 GiB 地址 space。 (内核保留的一两页除外:另请参阅
这不会在 64 位模式下发生:有足够的地址 space 可以在用户和内核之间平均分配它,并且在高范围和低范围之间有一个巨大的非规范孔.