理解简单C程序生成的汇编代码

时间:2010-09-06 23:46:55

标签: c linux assembly stack disassembly

我试图通过使用gdb的反汇编程序检查它来了解简单C程序的汇编级代码。

以下是C代码:

#include <stdio.h>

void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
}

void main() {
  function(1,2,3);
}

以下是mainfunction

的反汇编代码
gdb) disass main
Dump of assembler code for function main:
0x08048428 <main+0>:    push   %ebp
0x08048429 <main+1>:    mov    %esp,%ebp
0x0804842b <main+3>:    and    $0xfffffff0,%esp
0x0804842e <main+6>:    sub    $0x10,%esp
0x08048431 <main+9>:    movl   $0x3,0x8(%esp)
0x08048439 <main+17>:   movl   $0x2,0x4(%esp)
0x08048441 <main+25>:   movl   $0x1,(%esp)
0x08048448 <main+32>:   call   0x8048404 <function>
0x0804844d <main+37>:   leave  
0x0804844e <main+38>:   ret
End of assembler dump.

(gdb) disass function
Dump of assembler code for function function:
0x08048404 <function+0>:    push   %ebp
0x08048405 <function+1>:    mov    %esp,%ebp
0x08048407 <function+3>:    sub    $0x28,%esp
0x0804840a <function+6>:    mov    %gs:0x14,%eax
0x08048410 <function+12>:   mov    %eax,-0xc(%ebp)
0x08048413 <function+15>:   xor    %eax,%eax
0x08048415 <function+17>:   mov    -0xc(%ebp),%eax
0x08048418 <function+20>:   xor    %gs:0x14,%eax
0x0804841f <function+27>:   je     0x8048426 <function+34>
0x08048421 <function+29>:   call   0x8048340 <__stack_chk_fail@plt>
0x08048426 <function+34>:   leave  
0x08048427 <function+35>:   ret    
End of assembler dump.

我正在寻找以下事情的答案:

  1. 地址是如何工作的,我的意思是(主要+ 0),(主要+ 1),(主要+ 3)
  2. 主要是为什么$ 0xfffffff0,%esp正在使用
  3. 在函数中,为什么要使用%gs:0x14,%eax,%eax,-0xc(%ebp)。
  4. 如果有人能够一步一步地解释,那将非常感激。

4 个答案:

答案 0 :(得分:40)

main+0main+1main+3main+6等“奇怪”地址的原因是因为每条指令占用的变量数量为字节。例如:

main+0: push %ebp

是一个单字节指令,因此下一条指令位于main+1。另一方面,

main+3: and $0xfffffff0,%esp

是一个三字节指令,因此之后的下一条指令是main+6

而且,由于您在评论中询问为什么movl似乎采用可变数量的字节,因此对此的解释如下。

指令长度不仅取决于操作码(例如movl),还取决于操作数的寻址模式(操作码是经营)。我没有专门检查你的代码,但我怀疑

movl $0x1,(%esp)

指令可能更短,因为没有涉及偏移 - 它只使用esp作为地址。而类似的东西:

movl $0x2,0x4(%esp)

需要movl $0x1,(%esp)所做的一切,加上偏移0x4的额外字节。

实际上,这是一个显示我的意思的调试会话:

Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.

c:\pax> debug
-a
0B52:0100 mov word ptr [di],7
0B52:0104 mov word ptr [di+2],8
0B52:0109 mov word ptr [di+0],7
0B52:010E
-u100,10d
0B52:0100 C7050700      MOV     WORD PTR [DI],0007
0B52:0104 C745020800    MOV     WORD PTR [DI+02],0008
0B52:0109 C745000700    MOV     WORD PTR [DI+00],0007
-q
c:\pax> _

你可以看到带有偏移的第二条指令实际上与没有它的第一条指令不同。它长一个字节(5个字节而不是4个,用于保持偏移量)并且实际上具有不同的编码c745而不是c705

您还可以看到您可以用两种不同的方式对第一条和第三条指令进行编码,但它们基本上做同样的事情。


and $0xfffffff0,%esp指令是强制esp在特定边界上的一种方法。这用于确保变量的正确对齐。如果现代处理器遵循对齐规则(例如,4字节值必须与4字节边界对齐),那么现代处理器上的许多存储器访问将更有效。如果你不遵守这些规则,一些现代处理器甚至会引发错误。

在此指令之后,您可以保证esp小于或等于其先前的值与16字节边界对齐。


gs:前缀只是意味着使用gs段寄存器来访问内存而不是默认值。

指令mov %eax,-0xc(%ebp)表示获取ebp寄存器的内容,减去12(0xc),然后将eax的值放入该存储单元。< / p>


重新解释代码。你的function函数基本上是一个大的无操作。生成的程序集仅限于堆栈帧设置和拆除,以及一些使用上述%gs:14内存位置的堆栈帧损坏检查。

它将该位置的值(可能类似于0xdeadbeef)加载到堆栈框架中,完成其工作,然后检查堆栈以确保它没有被破坏。

在这种情况下,它的工作一无所获。所有你看到的是功能管理的东西。

堆栈设置发生在function+0function+12之间。之后的所有内容都是在eax中设置返回代码并拆除堆栈框架,包括损坏检查。

同样,main由堆栈帧设置组成,推送function的参数,调用function,拆除堆栈帧并退出。

评论已插入下面的代码中:

0x08048428 <main+0>:    push   %ebp                 ; save previous value.
0x08048429 <main+1>:    mov    %esp,%ebp            ; create new stack frame.
0x0804842b <main+3>:    and    $0xfffffff0,%esp     ; align to boundary.
0x0804842e <main+6>:    sub    $0x10,%esp           ; make space on stack.

0x08048431 <main+9>:    movl   $0x3,0x8(%esp)       ; push values for function.
0x08048439 <main+17>:   movl   $0x2,0x4(%esp)
0x08048441 <main+25>:   movl   $0x1,(%esp)
0x08048448 <main+32>:   call   0x8048404 <function> ; and call it.

0x0804844d <main+37>:   leave                       ; tear down frame.
0x0804844e <main+38>:   ret                         ; and exit.

0x08048404 <func+0>:    push   %ebp                 ; save previous value.
0x08048405 <func+1>:    mov    %esp,%ebp            ; create new stack frame.
0x08048407 <func+3>:    sub    $0x28,%esp           ; make space on stack.
0x0804840a <func+6>:    mov    %gs:0x14,%eax        ; get sentinel value.
0x08048410 <func+12>:   mov    %eax,-0xc(%ebp)      ; put on stack.

0x08048413 <func+15>:   xor    %eax,%eax            ; set return code 0.

0x08048415 <func+17>:   mov    -0xc(%ebp),%eax      ; get sentinel from stack.
0x08048418 <func+20>:   xor    %gs:0x14,%eax        ; compare with actual.
0x0804841f <func+27>:   je     <func+34>            ; jump if okay.
0x08048421 <func+29>:   call   <_stk_chk_fl>        ; otherwise corrupted stack.
0x08048426 <func+34>:   leave                       ; tear down frame.
0x08048427 <func+35>:   ret                         ; and exit.

我认为%gs:0x14的原因可能从上面可见,但为了以防万一,我将在此详述。

它使用这个值(一个sentinel)放入当前的堆栈帧,这样,如果函数中的某些内容做了愚蠢的事情,比如将1024字节写入堆栈中创建的20字节数组,或者在您的情况下:< / p>

char buffer1[5];
strcpy (buffer1, "Hello there, my name is Pax.");

然后将覆盖哨兵,并且函数末尾的检查将检测到,调用失败函数让你知道,然后可能中止,以避免任何其他问题。

如果它将0xdeadbeef放到堆栈上并且已将其更改为其他内容,那么带有xor的{​​{1}}将生成非零值,该值在代码中检测到0xdeadbeef指令。

相关位在这里解释:

je

答案 1 :(得分:3)

Pax已经提出了明确的答案。但是,为了完整起见,我想我会添加一个关于让GCC本身向你展示它所生成的程序集的说明。

GCC的-S选项告诉它停止编译并将程序集写入文件。通常,它会将该文件传递给汇编程序,或者某些目标直接写入目标文件。

问题中的示例代码:

#include <stdio.h>

void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
}

void main() {
  function(1,2,3);
}

命令gcc -S q3654898.c创建一个名为q3654898.s的文件:

        .file   "q3654898.c"
        .text
.globl _function
        .def    _function;      .scl    2;      .type   32;     .endef
_function:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $40, %esp
        leave
        ret
        .def    ___main;        .scl    2;      .type   32;     .endef
.globl _main
        .def    _main;  .scl    2;      .type   32;     .endef
_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $24, %esp
        andl    $-16, %esp
        movl    $0, %eax
        addl    $15, %eax
        addl    $15, %eax
        shrl    $4, %eax
        sall    $4, %eax
        movl    %eax, -4(%ebp)
        movl    -4(%ebp), %eax
        call    __alloca
        call    ___main
        movl    $3, 8(%esp)
        movl    $2, 4(%esp)
        movl    $1, (%esp)
        call    _function
        leave
        ret

显而易见的一点是,我的GCC(gcc(GCC)3.4.5(mingw-vista special r3))默认情况下不包含堆栈检查代码。我想有一个命令行选项,或者说,如果我有可能将我的MinGW安装到最新的GCC,那么它就可以了。

编辑:由Pax推动这样做,这是让GCC完成更多工作的另一种方法。

C:\Documents and Settings\Ross\My Documents\testing>gcc -Wa,-al q3654898.c
q3654898.c: In function `main':
q3654898.c:8: warning: return type of 'main' is not `int'
GAS LISTING C:\DOCUME~1\Ross\LOCALS~1\Temp/ccLg8pWC.s                   page 1


   1                            .file   "q3654898.c"
   2                            .text
   3                    .globl _function
   4                            .def    _function;      .scl    2;      .type
32;     .endef
   5                    _function:
   6 0000 55                    pushl   %ebp
   7 0001 89E5                  movl    %esp, %ebp
   8 0003 83EC28                subl    $40, %esp
   9 0006 C9                    leave
  10 0007 C3                    ret
  11                            .def    ___main;        .scl    2;      .type
32;     .endef
  12                    .globl _main
  13                            .def    _main;  .scl    2;      .type   32;
.endef
  14                    _main:
  15 0008 55                    pushl   %ebp
  16 0009 89E5                  movl    %esp, %ebp
  17 000b 83EC18                subl    $24, %esp
  18 000e 83E4F0                andl    $-16, %esp
  19 0011 B8000000              movl    $0, %eax
  19      00
  20 0016 83C00F                addl    $15, %eax
  21 0019 83C00F                addl    $15, %eax
  22 001c C1E804                shrl    $4, %eax
  23 001f C1E004                sall    $4, %eax
  24 0022 8945FC                movl    %eax, -4(%ebp)
  25 0025 8B45FC                movl    -4(%ebp), %eax
  26 0028 E8000000              call    __alloca
  26      00
  27 002d E8000000              call    ___main
  27      00
  28 0032 C7442408              movl    $3, 8(%esp)
  28      03000000
  29 003a C7442404              movl    $2, 4(%esp)
  29      02000000
  30 0042 C7042401              movl    $1, (%esp)
  30      000000
  31 0049 E8B2FFFF              call    _function
  31      FF
  32 004e C9                    leave
  33 004f C3                    ret

C:\Documents and Settings\Ross\My Documents\testing>

这里我们看到汇编器生成的输出列表。 (它的名字是GAS,因为它是经典* nix汇编程序as的Gnu版本。那里有幽默。)

每行包含以下大部分字段:行号,当前节中的地址,存储在该地址的字节以及汇编源文件中的源文本。 地址偏移到此模块提供的每个部分的该部分。此特定模块仅在.text部分中包含存储可执行代码的内容。您通常会发现提及名为.data.bss的部分。使用了许多其他名称,其中一些具有特殊用途。如果您真的想知道,请阅读链接器的手册。

答案 2 :(得分:3)

最好使用gcc尝试 -fno-stack-protector 标志来禁用金丝雀并查看结果。

答案 3 :(得分:2)

我想补充一点,对于简单的东西,如果你打开一点优化,GCC的汇编输出通常更容易阅读。这是示例代码......

void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
}

/* corrected calling convention of main() */
int main() {
   function(1,2,3);
   return 0;
}

这是我没有优化的结果(OSX 10.6,gcc 4.2.1 + Apple补丁)

.globl _function
_function:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    call    L4
"L00000000001$pb":
L4:
    popl    %ebx
    leal    L___stack_chk_guard$non_lazy_ptr-"L00000000001$pb"(%ebx), %eax
    movl    (%eax), %eax
    movl    (%eax), %edx
    movl    %edx, -12(%ebp)
    xorl    %edx, %edx
    leal    L___stack_chk_guard$non_lazy_ptr-"L00000000001$pb"(%ebx), %eax
    movl    (%eax), %eax
    movl    -12(%ebp), %edx
    xorl    (%eax), %edx
    je      L3
    call    ___stack_chk_fail
L3:
    addl    $36, %esp
    popl    %ebx
    leave
    ret
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $24, %esp
    movl    $3, 8(%esp)
    movl    $2, 4(%esp)
    movl    $1, (%esp)
    call    _function
    movl    $0, %eax
    leave
    ret
哎呀,一口一口!但是看看命令行上-O会发生什么......

    .text
.globl _function
_function:
    pushl   %ebp
    movl    %esp, %ebp
    leave
    ret
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    movl    $0, %eax
    leave
    ret

当然,您确实存在使代码呈现完全无法识别的风险,尤其是在更高的优化级别和更复杂的内容中。即使在这里,我们也看到对function的调用被忽略了。但我发现,不必阅读数十次不必要的堆栈溢出通常比控制流程更有价值。