如何将C中的NULL推入汇编中的堆栈?

时间:2019-06-05 17:33:33

标签: c assembly x86 nasm

我正在用汇编语言编写冒泡排序以对字符串进行排序,并且我正在使用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等于什么?

3 个答案:

答案 0 :(得分:5)

 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?

答案 1 :(得分:3)

  

我已经读过,在大多数实现中,NULL都指向0,无论这意味着什么。

否, 0;它不是 任何东西的指针。

在C源代码中,(void*)0始终为NULL,但是允许实现在内部为int *p = NULL;的对象表示使用不同的非零位模式。选择非零位模式的实现需要在编译时进行转换。 (并且 only 转换仅在编译时适用于指针上下文中出现的值为零的编译时整数常量表达式,不适用于memset或其他任何形式。) C ++常见问题解答在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传递到寄存器中。)

@J ...的答案显示了如何按从右到左的顺序按所使用的调用约定推送参数,从而导致第一个(最左侧)参数最低的地址。

实际上,只要它们在mov运行时以正确的位置结束,您就可以将它们存储到堆栈中(例如,call)。


C标准允许它有所不同,因为某些硬件上的C实现可能要使用其他功能,例如一种特殊的位模式,无论上下文如何,在引用后始终会出错。或者,如果0在实际程序中是有效的地址值,那么对于有效的指针,p==NULL始终为false会更好。或任何其他特定于硬件的奥秘原因。

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

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

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

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

  

Prime 50系列使用段07777,对于空指针,至少对于PL / I,偏移0。后来的模型使用段0,对于C中的空指针,偏移量为0,因此需要诸如TCNP(测试C空指针)之类的新指令,显然是[脚注]所有现存的不良C语言的补充错误假设的代码。较早的以字寻址的Prime机器也臭名昭著,因为它需要比字指针(char *)大的字节指针(int *)。

     

...有关更多机器,请参见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的地址空间。这有助于它是否来自源中的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

答案 2 :(得分:1)

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

问题1

  

我已经读到了……NULL指向0,无论什么意思。

这意味着几乎所有C编译器都将 class UsernameTextField extends StatefulWidget{ final usernameController = TextEditingController(); UsernameTextField(this.usernameController) @override State<StatefulWidget> createState() { return UsernameTextFieldState(usernameController); } } class UsernameTextFieldState extends State<UsernameTextField>{ @override Widget build(BuildContext context) { return AppTextField( decoration: InputDecoration( contentPadding: const EdgeInsets.all(20.0), labelText: AppTranslations.of(context) .text("loginpage_username"), ), myController: widget.usernameController, textInputType: TextInputType.emailAddress ); } } 定义为NULL

这意味着(void *)0指针是指向地址为零的内存位置的指针。

  

我在“大多数实现”中都读过它……

“最多”表示 1980年代末引入ISO C和ANSI C之前,有C编译器以不同的方式定义了NULL方式。

也许仍然有一些非标准 C编译器无法将地址0识别为NULL

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

问题2

  

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

指针是地址。

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

您通过推动整数值0来推动NULL指针。

NULL

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

在这种情况下,不同的汇编器可能以两种不同的方式解释指令NULL equ 0 push NULL

  • 某些汇编程序会将其解释为:“ 将值推为0 ”。

    这是正确的。

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

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