我试图通过使用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.
我正在为以下事情寻找答案:
奇怪的地址,例如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
的值写入该存储单元。
重新解释代码。 你的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
的原因,但为了以防万一,我会在这里详细说明。
它使用这个值(一个哨兵)来放入当前的栈帧,这样,如果函数中的东西做了一些傻事就像写1024字节到堆栈上创建的20字节数组,或者在你的情况下:
char buffer1[5]; strcpy (buffer1, "Hello there, my name is Pax.");
那么标记将被覆盖,并且在函数结尾的检查将检测到,调用失败函数让你知道,然后可能中止,以避免任何其他问题。
如果它将0xdeadbeef
放到堆栈上并将其更改为其他内容,则带有0xdeadbeef
的xor
将产生一个非零值,该值在je
指令的代码中被检测到。
相关位在这里被解释:
mov %gs:0x14,%eax ; get sentinel value. mov %eax,-0xc(%ebp) ; put on stack. ;; Weave your function ;; magic here. mov -0xc(%ebp),%eax ; get sentinel back from stack. xor %gs:0x14,%eax ; compare with original value. je stack_ok ; zero/equal means no corruption. call stack_bad ; otherwise corrupted stack. stack_ok: leave ; tear down frame.
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” 。文本 .globl _function .def _function; .scl 2; .type 32; .endef伪 _功能: pushl%ebp movl%esp,%ebp subl $ 40,%esp 离开 RET .def ___main; .scl 2; .type 32; .endef伪 .globl _main .def _main; .scl 2; .type 32; .endef伪 _主要: 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 调用__alloca 调用___main movl $ 3,8(%esp) movl $ 2,4(%esp) movl $ 1,(%esp) 调用_function 离开 RET
有一点很明显,就是我的GCC(gcc(GCC)3.4.5(mingw-vista special r3))默认不包含堆栈检查代码。 我想象有一个命令行选项,或者如果我想将我的MinGW安装到一个更新的GCC,它可以。
编辑:帕克斯推动这样做,这是让海湾合作委员会做更多的工作的另一种方式。
C:\ Documents and Settings \ Ross \ My Documents \ testing> gcc -Wa,-al q3654898.c q3654898.c:在函数`main'中: q3654898.c:8:警告:'main'的返回类型不是'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; 。类型 32; .endef伪 5 _function: 6 0000 55 pushl%ebp 7 0001 89E5 movl%esp,%ebp 8 0003 83EC28 subl $ 40,%esp 9 0006 C9离开 10 0007 C3 ret 11.def ___main; .scl 2; 。类型 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致电__alloca 26 00 27 002d E8000000呼叫___主 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调用_function 31 FF 32 004e C9离开 33 004f C3 ret C:\ Documents and Settings \ Ross \ My Documents \ testing>
这里我们看到汇编程序产生的输出列表。 (它的名字是GAS
,因为它是Gnu的经典* nix汇编程序的版本,在那里有幽默。)
每行包含以下大部分字段:行号,当前段中的地址,存储在该地址中的字节以及汇编源文件中的源文本。 地址偏移到由该模块提供的每个部分的那部分。 这个特定的模块只包含存储可执行代码的.text
部分的内容。 您通常会找到名为.data
和.bss
的部分。 许多其他的名字被使用,有些有特殊的用途。 如果你真的想知道,请阅读链接器手册。
使用gcc尝试-fno-stack-protector标志来禁用canary并查看结果会更好。
我想补充的是,对于简单的东西,如果你打开一个小优化,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 +苹果补丁)
.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
的呼唤已被废弃为毫无意义。 但是我发现,不必通读数十个不必要的堆栈溢出,通常比控制流程更值得我头痛。