【问题标题】:How do I push the equivalent of NULL in C to the stack in assembly?如何将 C 中的 NULL 等价物推送到汇编中的堆栈?
【发布时间】:2019-06-05 17:33:33
【问题描述】:

我正在为汇编语言中的字符串排序编写冒泡排序,并且我正在使用 strtok() 来标记字符串。但是,在第一次调用strtok(str," ")之后,我需要传递NULL作为参数,即strtok(NULL," ")

我在 .bss 段中尝试了 NULL equ 0 但这没有任何作用。

[SECTION .data]

[SECTION .bss]

string resb 64
NULL equ 0

[SECTION .text]

extern fscanf
extern stdin
extern strtok

global main

main:

    push ebp        ; Set up stack frame for debugger
    mov ebp,esp
    push ebx        ; Program must preserve ebp, ebx, esi, & edi
    push esi
    push edi

    push cadena
    push frmt
    push dword [stdin]      ;Read string from stdin
    call fscanf
    add esp,12              ;clean stack

    push delim
    push string             ;this works
    call strtok
    add esp,8               ;clean stack

    ;after this step, the return value in eax points to the first word 

    push string             ;this does not
    push NULL
    call strtok
    add esp,8               ;clean stack

    ;after this step, eax points to 0x0

    pop edi         ; Restore saved registers
    pop esi
    pop ebx
    mov esp,ebp     ; Destroy stack frame before returning
    pop ebp
    ret         ;return control to linux

我在“大多数实现”中读到过,NULL 指向 0,不管这意味着什么。为什么会有歧义? x86指令集中的NULL相当于什么?

【问题讨论】:

  • 记住你推送参数的顺序...
  • NULL 不指向零,它是零,它不指向任何地方。但正如已经指出的那样,问题在于您的论点顺序。

标签: c assembly x86 nasm null-pointer


【解决方案1】:
 push NULL 
 push string 
 call strtok

这是调用strtok(string, NULL)。你想要strtok(NULL, " "),所以假设delim 包含" "

 push delim
 push NULL
 call strtok

cdecl 调用约定中,参数以相反的(从右到左)顺序进入堆栈。


对于您问题的另一部分(NULL 始终为零),请参阅:Is NULL always zero in C?

【讨论】:

  • 谢谢,我没注意到。但是,在进行此更改并重新编译后仍然没有更改。这是否与从已编译的 c 程序外部调用 strtok 有关?或者 NULL 可以是别的吗?也许它必须这样做,我正在为 32 位架构编译我的程序,但我的 C 实现是针对 64 位架构的?
  • @CarlosCarral 也许,也许,也许。您没有告诉我们太多有关您的环境、操作系统等的信息。minimal reproducible example 和第二个问题可能是最好的方法。
  • @CarlosCarral:在所有 x86 调用约定/ABI 中,NULL 指针的 asm 位模式是整数 0。所以push 0 在x86 上总是安全的。 C 标准允许它有所不同,因为 some 硬件可能想要使用其他东西,例如总是出错的位模式。这不是在 x86 上完成的。 (尽管事实上某些操作系统,尤其是 Windows 95,将零页映射到用户空间进程的地址空间,因此 NULL 指针取消引用的未定义行为可能会破坏整个机器状态,而不仅仅是导致该进程出错! )
【解决方案2】:

我在“大多数实现”中读到,NULL 指向 0,不管这意味着什么。

不,它 0;它不是 任何东西的指针。所以是的,NULL equ 0 是正确的,或者只是push 0

在 C 源代码中,(void*)0 始终为 NULL,但允许实现在内部为 int *p = NULL; 的对象表示使用不同的非零位模式。选择非零位模式的实现需要在编译时进行转换。 (并且翻译在编译时适用于在指针上下文中出现的值为 0 的编译时整数常量表达式,不适用于 memset 或其他。)C++ FAQ 有一个NULL pointers 上的整个部分。 (在这种情况下也适用于 C。)

(在 C 语言中,使用 memcpy 将对象的位模式访问为整数或使用 (char*) 别名访问对象的位模式是合法的,因此可以在没有未定义行为的格式良好的程序中检测到这一点. 或者当然是通过调试器查看 asm 或内存内容!在实践中,您可以通过编译 int*foo(){return NULL;} 轻松检查 NULL 的正确 asm )

有关更多背景信息,另请参阅 Why is address zero used for the null pointer?

为什么会有歧义? x86指令集中的NULL相当于什么?

在所有 x86 调用约定/ABI 中,NULL 指针的 asm 位模式是整数 0

所以 push 0xor edi,edi (RDI=0) 在 x86 / x86-64 上始终是您想要的。(现代调用约定,包括所有 x86-64 约定,传入 args寄存器。)Windows x64 传递 RCX 中的第一个参数,而不是 RDI。


@J... 的回答展示了如何为您正在使用的调用约定按从右到左的顺序推送参数,从而导致第一个(最左边的)arg 在最低的地址。

真的,您可以随意将它们存储到堆栈中(例如使用mov),只要它们在call 运行时最终位于正确的位置。


C 标准允许它有所不同,因为某些硬件上的 C 实现可能想要使用其他东西,例如一种特殊的位模式,在取消引用时总是出错,无论上下文如何。或者,如果0 是实际程序中的有效地址值,则最好p==NULL 对于有效指针始终为假。或任何其他神秘的硬件特定原因。

所以是的,可能已经有一些用于 x86 的 C 实现,其中 C 源代码中的 (void*)0 变成 asm 中的非零整数。但实际上并没有。 (大多数程序员很高兴memset(array_of_pointers, 0, size) 实际上将它们设置为 NULL,这依赖于 0 的位模式,因为某些代码做出了这样的假设,而没有考虑到它不能保证可移植的事实。

这不是在 x86 上的任何标准 C ABI 中完成的。 (ABI 是编译器都遵循的一组实现选择,因此它们的代码可以相互调用,例如就结构布局、调用约定以及p == NULL 的含义达成一致。)

我不知道在其他 32 位或 64 位 CPU 上使用非零 NULL 的任何现代 C 实现;虚拟内存可以轻松避开地址 0。

http://c-faq.com/null/machexamp.html有一些历史例子:

Prime 50 系列使用段 07777,偏移量 0 作为空指针,至少对于 PL/I。后来的模型使用段 0,C 中空指针的偏移量为 0,需要新的指令,例如 TCNP(测试 C 空指针),显然是为了 [脚注] 所有现存的写得不好的 C 代码不正确假设。旧的字寻址 Prime 机器也因需要比字指针 (int *) 更大的字节指针 (char *) 而臭名昭著。

...有关更多机器,请参阅the link,以及本段的脚注。

https://www.quora.com/On-which-actual-architectures-is-Cs-null-pointer-not-a-binary-zero-all-bits-zero 报告在 286 Xenix 上发现了一个非零 NULL,我猜是使用分段指针。


现代 x86 操作系统确保进程无法将任何内容映射到虚拟地址空间的最低页面,因此 NULL 指针取消引用总是会出现故障以使调试更容易。

例如默认情况下,Linux 保留低 64kiB 的地址空间 (vm.mmap_min_address)。这有助于它是否来自源中的 NULL 指针,或者是否有其他错误将带有整数零的指针归零。 64k 而不仅仅是低 4k 页面捕获将指针作为数组进行索引,例如 p[i] 具有小到中的 i 值。

有趣的事实:Windows 95 将用户空间虚拟地址空间的最低页面映射到物理内存的前 64kiB 以解决 386 B1 步进错误。但幸运的是,它能够进行设置,因此从正常的 32 位进程访问确实出错了。尽管如此,在 DOS 兼容模式下运行的 16 位代码很容易破坏整个机器。

https://devblogs.microsoft.com/oldnewthing/20141003-00/?p=43923https://news.ycombinator.com/item?id=13263976

【讨论】:

  • Win32 progs 有第一个 64KiB 映射,但无法无故障处理。 Win16 的近端指针是相对于 64kb 段的开始而言的,这当然不是物理内存的第一个 64KiB。 Win16 远指针是选择器:偏移量。选择器 0 导致访问 NULL 描述符时出错 Win32 会出错,因为数据选择器向下扩展。真正的问题是在 MS-DOS 兼容性会话中运行的代码可能会破坏前 1MiB 的内存。当然,Win16 和 Win32 进程可以做一些事情来规避有限的保护。
  • @MichaelPetch:哦,对于“普通”代码,这比我想象的要疯狂得多。和 IIRC,它不是 NULL deref,而是数组溢出错误,我记得在 2000 年夏天导致 Win9x 上的系统范围锁定(MinGW 或 Cygwin 用于仅使用 scanf/printf 的命令行测试程序和一些数学库函数来测试我在 Excel 工作后会调用的函数。)所以零页的东西并不能解释它。
  • 就个人而言,我一直在等待 OP 放弃 “哦,它(插入深奥的实时嵌入式 x86 平台)具有原型 C 实现,每晚构建......”。无论如何,很好的答案。
  • @J...:呵呵,如果是这样的话,我希望他们会选择比堆栈参数更糟糕的调用约定!至少有几个寄存器参数可能是优化代码的代码大小的胜利,通常无论如何都会将内容保存在寄存器中。并且肯定是整体性能的胜利。对于最大的 C 陷阱,包括没有全零位模式的 NULL,也许“哦,这是一个 DeathStation 9000 夜间构建”。 :P
  • @PeterCordes 是的,后者!真的,我只是希望 OP 会编辑他们的一些平台细节,这样我们就不必猜测了。
【解决方案3】:

你实际上是在问两个问题:

问题 1

我读到过……NULL 指向 0,不管这意味着什么。

这意味着几乎所有 C 编译器都将 NULL 定义为 (void *)0

这意味着NULL 指针是指向地址为零的内存位置的指针。

我在“大多数实现”中读到过这一点......

“大多数”表示1980年代后期引入ISO C和ANSI C之前,有C编译器以不同的方式定义NULL

也许一些非标准的 C 编译器仍然存在不将地址 0 识别为NULL

但是,您可以假设您的 C 编译器和您在汇编项目中使用的 C 库将 NULL 定义为指向地址 0 的指针。

问题 2

如何将 C 中的 NULL 等价物推送到汇编中的堆栈?

指针就是地址。

(与其他一些 CPU 不同),x86 CPU 不区分整数和地址:

您通过压入整数值 0 来压入 NULL 指针。

NULL equ 0

push NULL

不幸的是,您没有编写您使用的汇编程序。 (其他用户认为它是 NASM。)

在这种情况下,指令push NULL 可能被不同的汇编程序以两种不同的方式解释:

  • 一些汇编器会将其解释为:“Push value 0”。

    这是正确的。

  • 其他汇编程序会将其解释为:“读取内存位置 0 处的内存并推送该值

    这将等于 C 中的 someFunction(*(int *)NULL),因此会导致异常(NULL 指针访问)。

【讨论】:

  • 这是 NASM 语法,NULL equ 0 加上 push NULL 归结为 push 0。在 AT&T 语法中,这将是 push $0。这个答案没有帮助。 NULL equ 0 正确地将 NULL 定义为 value 0 的汇编时常量,就像在 GAS 中的 .equ NULL, 0 一样。这就是您使用汇编时命名常量的方式。
  • 在所有 ISO C 编译器中,(void *)0 是一个 NULL 指针常量。 NULL 宏没有必须以这种方式定义,但是指针上下文中的整数 0 需要编译为用于表示 NULL 指针的任何位模式,无论是或者不是全零。在使用全一作为其 NULL 的机器上,(void *)0xFFFFFFFF 可能是 NULL 的有效定义,但您的措辞令人困惑,并暗示 (void*)0 在这样的情况下不会为 NULL机器。
  • @PeterCordes 有很多不同的汇编器,它们的语法非常相似,但对某些指令的解释不同,因此一个汇编器将 push xyz 解释为 push [xyz](NASM?),另一个解释与push offset xyz 相同的源代码行(就像早期的 Microsoft 汇编器所做的那样)。我刚刚检查了 GAS(2.24 版),AT&T 风格:.equ NULL, 0 后跟 push NULL 将生成与 push 0 相同的代码,即英特尔语法中的 push [ds:0];要推送值 0,您必须写 push $NULL,而不是 push NULL。 (我不确定这是否在较新的 GAS 版本中发生了变化)。
  • @PeterCordes 我说的是 1970 年代的 C 编译器。 ANSI C 和 ISO C 于 1989 年开始存在。我怀疑将 NULL 定义为 *(void *)0xFFFFFFFF 的 C 编译器是否符合 ISO 或 ANSI。据我记得,甚至有一个编译器提到使用像#define NULL (void *)0xEF000000 这样的“特殊”模式。这只有在(void *)0(void *)0xFFFFFFFF有效、机器和操作系统上的非NULL 指针时才有意义。
  • @PeterCordes 我完全编辑了我的答案并删除了很多令人困惑的文字。
猜你喜欢
  • 2022-01-16
  • 1970-01-01
  • 2013-12-29
  • 2014-08-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-04-26
  • 1970-01-01
相关资源
最近更新 更多