【问题标题】:Why my own memcpy written on NASM can not copy more than 340000000 bytes?为什么我自己写在 NASM 上的 memcpy 不能复制超过 340000000 字节?
【发布时间】:2021-07-22 15:16:26
【问题描述】:

我正在学习 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 操作系统。当我用$ nasm -f elf m.asm &amp;&amp; gcc -Wall -m32 m.o p.c &amp;&amp; ./a.out 编译和链接这两个文件时,当Times 的值小于340000000 时,它可以正常工作。当它更大时,_myMemcpy 仅将源的第一个字节复制到目标。我无法弄清楚问题出在哪里。每个建议都会很有用。

【问题讨论】:

  • 您正在对指针进行有符号比较;对于像size = 0x1443fd00 这样的巨大数组,其中一个将跨越 2GiB 边界(有符号环绕),除非链接器特别注意将一个放在高半部分,另一个放在低半部分。但它不会,它会使 .bss 连续。
  • 所以将jl 替换为jb。让我们知道它是否有效!
  • eax 是 32 位寄存器,Times 是 64 位
  • @PeterCordes,谢谢!它适用于jb
  • @PaulHankin:这是 32 位代码;当使用 32 位指针作为地址时,您可以通过堆栈参数和不分段错误来判断。 Times 是一个宏:3400000000x1443fd00。作为 C 字面量,它适合 32 位 int,因此它在任何 C 实现中都具有类型 int,且 int 至少为 32 位。

标签: c pointers assembly x86 nasm


【解决方案1】:

您正在对指针进行有符号比较;不要那样做。在这种情况下使用jne,因为您将始终在退出点达到完全相等。

或者,如果您想要与指针进行关系比较,通常像 jbjae 这样的无符号条件最有意义。 (通常将虚拟地址空间视为平坦的线性 4GiB,最低地址为 0,因此您需要在该范围的中间递增才能工作)。

如果数组大于您的 ~300MiB 大小,并且 PIE 可执行文件的默认链接器脚本,显然其中之一将跨越有符号正数和有符号负数之间的 2GiB 边界1。因此,如果您将其视为有符号整数,您计算的端点将是“负数”。 (不像在 x86-64 上,跨越虚拟地址空间中间的非规范“洞”意味着数组永远不能跨越有符号环绕边界:Should pointer comparisons be signed or unsigned in 64-bit x86? - 有时它确实在那里使用带符号的比较是有意义的。)

如果您单步查看指针值以及使用size += dest (add [esp + 12], eax) 创建的内存值,您应该会在调试器中看到这一点。作为一个有符号的操作,它会溢出来创建一个负的 end_pointer,而起始指针仍然是正的。 pos &lt; neg 在第一次迭代时为 false,因此您的循环退出,您可以在单步执行时看到这一点。


脚注 1:在我的系统上,在 GDB(禁用 ASLR)下,在 start 之后将可执行文件映射到 Linux 的 PIE 的默认基地址(2/3 到地址空间的低一半,即 0x5555...),我用您的测试用例检查了地址:

  • sr0x56559040
  • ds0x6a998d40
  • ds 结束于 p /x sizeof(ds) + ds = 0x7edd8a40

因此,如果它更大,它将跨越0x80000000。这就是为什么340000000 避免了你的错误,但更大的尺寸会暴露它。

顺便说一句,在 32 位内核下,Linux 默认在内核和用户空间之间以 3:1 的比例分割地址空间,因此即使存在这种情况也有可能发生。但是在 64 位内核下,32 位进程可以拥有整个 4 GiB 的地址空间。 (除了内核保留的一两个页面:另见Why can't I mmap(MAP_FIXED) the highest virtual page in a 32-bit Linux process on a 64-bit kernel?。这也意味着像你正在做的那样形成一个指向任何数组的过去端的指针(ISO C 承诺这样做是有效的),不会回绕,仍然会在指向对象的指针上方进行比较。)

这在 64 位模式下不会发生:有足够的地址空间在用户和内核之间平均分配它,并且在高范围和低范围之间存在一个巨大的非规范漏洞。

【讨论】:

  • 一个很好的答案。但我对你声称 0x1443fd00 超过 2GiB 的一半感到困惑。
  • @TonyK:哦,是的,0x14... 不是 1.4GiB。德普。此外,OP 说这个尺寸 没有 有问题,但它是他们发现可以正常工作的最大尺寸。并不是说 this 大小会导致问题。替换为正确解释为什么较大的尺寸会推动数组末尾超过 2GiB。
猜你喜欢
  • 2023-04-03
  • 2019-09-07
  • 2019-03-28
  • 2019-12-09
  • 2021-09-06
  • 1970-01-01
  • 1970-01-01
  • 2018-10-18
  • 1970-01-01
相关资源
最近更新 更多