我试图通过使用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);
}
以下是main
和function
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.
我正在寻找以下事情的答案:
答案 0 :(得分:40)
main+0
,main+1
,main+3
,main+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+0
和function+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
的调用被忽略了。但我发现,不必阅读数十次不必要的堆栈溢出通常比控制流程更有价值。