空字符汇编代码

时间:2017-04-01 11:26:44

标签: assembly gdb

  1. 0x00000000004005c7 <+28>: movw $0x0,0x8(%rsp)是否在字符串末尾添加空字符?

  2. 有人还可以解释前4行吗?

    0x00000000004005ab <+0>:     sub    $0x28,%rsp
    0x00000000004005af <+4>:     mov    %fs:0x28,%rax
    0x00000000004005b8 <+13>:    mov    %rax,0x18(%rsp)
    0x00000000004005bd <+18>:    xor    %eax,%eax
    0x00000000004005bf <+20>:    movq   $0x64636261,(%rsp)
    0x00000000004005c7 <+28>:    movw   $0x0,0x8(%rsp)
    => 0x00000000004005ce <+35>:    mov    %rsp,%rdi
    0x00000000004005d1 <+38>:    callq  0x40059d <func>
    0x00000000004005d6 <+43>:    mov    $0x0,%eax
    0x00000000004005db <+48>:    mov    0x18(%rsp),%rdx
    0x00000000004005e0 <+53>:    xor    %fs:0x28,%rdx
    0x00000000004005e9 <+62>:    je     0x4005f0 <main+69>
    0x00000000004005eb <+64>:    callq  0x400480         <__stack_chk_fail@plt>
    0x00000000004005f0 <+69>:    add    $0x28,%rsp
    0x00000000004005f4 <+73>:    retq   
    
  3. C代码:

    #include <stdio.h>
    
    void func(char s[])
    {
        printf("%s\n", s);
    }
    
    int main()
    {
        char s[10] = "abcd";
    
        func(s);
    
        return 0;
    }
    

    感谢。

    操作系统:

    • Linux版本4.9.17-c9(gcc版本4.9.2(Debian 4.9.2-10))

    CPU:

    • vendor_id:GenuineIntel

    • cpu family:6

    • 型号:63

    • 型号名称:Intel(R)Xeon(R)CPU @ 2.30GHz

1 个答案:

答案 0 :(得分:4)

是的,这会将NUL字符添加到字符串的末尾。实际上,它是整个字符数组的零填充 - 请继续阅读以获取更多详细信息。

从阅读单个指令可以明显看出它在内存中存储了一个0,尽管你不能告诉它实际上是把它放在字符串的末尾。

movw   $0x0,0x8(%rsp)

您可以在此处看到此说明执行 w ord mov e。具体来说,它将立即值0($0x0)移动到内存位置0x8(%rsp),这是与rsp寄存器中的地址相差8字节。

如果展开检查代码的上下文,事情会变得更加清晰。考虑前面的说明:

movq   $0x64636261,(%rsp)

这对存储在0x64636261寄存器中的内存位置的立即值rsp执行 q uad-word mov e。当然,这个直接值是字符串"abcd"

现在,字符是单字节,0x64636261是4字节,就像字符串"abcd"一样。世界上为什么要在这里完成一个8字节的移动?好吧,因为编译器正在利用隐式零扩展行为。当它使用具有双字立即数的四字移动指令时,双字立即被隐式零扩展为四字。所以你实际做的是将0x0000000064636261移到(%rsp)

单词move指令也是零扩展:单字节立即数值隐式零扩展为一个完整的单词,然后单词0x0000被移动到0x8(%rsp)的内存中。 / p>

所有在一起,然后我们将10个字节移动到内存中:来自四字移动的8个字节,以及来自移动的字的2个字节。这个数字应该看起来很熟悉---它是你在C代码中声明的s数组的大小!

C语言的基本规则是:

  

&#34;如果字符串文字中有少量字符用于初始化已知大小的数组而不是数组中的元素,则[数组]的其余部分应隐式初始化为与对象相同具有静态存储持续时间。&#34;

     

(C99 $ 6.7.8 / 21

这实际上意味着数组的 rest 填充了0。

该数组的前4个字节用字符串"abcd"填充,然后接下来的6个字节用0填充。汇编代码只是以最佳方式打破存储:首先,它执行最大可能的存储,然后它执行最大可能的存储,它可以在不超出阵列的最大长度的情况下逃脱。

至于其余的代码,让我们逐行浏览:

  • sub $0x28,%rsp

rsp是包含堆栈指针的寄存器。这是从堆栈指针中减去0x28字节,有效地在堆栈上保留40个字节的空间,以便在本地使用该函数。它明确地使用10个字节左右;调用约定可能需要空间的其余部分,或者将其分配为优化以保持对齐。

  • mov %fs:0x28,%rax

这将从%fs:0x28检索值并将其存储在%rax中。 fs是段寄存器,0x28是偏移量。现代32位和64位操作系统不像现有的16位实模式那样使用分段寻址,但fs通常用于线程本地存储。因此代码从线程局部存储块的开头读取偏移量0x28处的值,并将其放在rax寄存器中。

  • mov %rax,0x18(%rsp)

这会将rax(我们刚刚加载的那个)的值存储到内存中。具体来说,它将它加载到堆栈上,与堆栈指针(rsp)的偏移量为0x18。

我猜这两行代码实现了某种类型的堆栈canary,但我不能确定没有关于你的操作系统,编译器设置等的更多信息。我的编译器没有&#39;在编译代码时生成这样的代码。

  • xor %eax,%eax

这个很简单,但有点模糊。将寄存器与其自身进行按位异或运算是将寄存器内容置零的一个老技巧。它也是by far the most optimal way of doing it,因此这是所有编译器都会生成的代码。

现在,它可能看起来有点奇怪,它只是将32位eax寄存器归零,而不是整个64位rax寄存器,但事实上,它正在这样做。 Virtually all instructions that operate on 32-bit registers in long mode implicitly zero the upper 32-bit half of the register。这是架构级别的重要优化,并且由于编译器知道处理器将要执行此操作,因此它会发出利用它的代码。 32位XOR指令较小,因此比发出xor %rax,%rax时更快,但行为相同。

为什么编译器发出代码来清除rax / eax寄存器?因为,在我所知道的所有x86调用约定中,该寄存器用于函数的返回值。您的main函数返回0,因此编译器正在安排该返回值位于rax寄存器中。