最近我对拆解C代码(非常简单的C代码)感兴趣,并且遵循使用Borland C ++ Compiler v 5.5(编译C代码就好)的教程,一切正常。然后我决定尝试自己的c代码并在Dev C ++(使用gcc)中编译它们。在IDA Pro中打开它后,我感到惊讶,与Borland相比,gcc的asm真的与众不同。我预计会有一些差异,但C代码非常简单,所以只是gcc没有优化那么多,还是他们使用不同的默认编译器设置?
C代码
int main(int argc, char **argv)
{
int a;
a = 1;
}
Borland ASM
.text:00401150 ; int __cdecl main(int argc,const char **argv,const char *envp)
.text:00401150 _main proc near ; DATA XREF: .data:004090D0
.text:00401150
.text:00401150 argc = dword ptr 8
.text:00401150 argv = dword ptr 0Ch
.text:00401150 envp = dword ptr 10h
.text:00401150
.text:00401150 push ebp
.text:00401151 mov ebp, esp
.text:00401153 pop ebp
.text:00401154 retn
.text:00401154 _main endp
GCC ASM(更新的BELLOW)
.text:00401220 ; ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦ S U B R O U T I N E ¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦
.text:00401220
.text:00401220 ; Attributes: bp-based frame
.text:00401220
.text:00401220 public start
.text:00401220 start proc near
.text:00401220
.text:00401220 var_14 = dword ptr -14h
.text:00401220 var_8 = dword ptr -8
.text:00401220
.text:00401220 push ebp
.text:00401221 mov ebp, esp
.text:00401223 sub esp, 8
.text:00401226 mov [esp+8+var_8], 1
.text:0040122D call ds:__set_app_type
.text:00401233 call sub_401100
.text:00401238 nop
.text:00401239 lea esi, [esi+0]
.text:00401240 push ebp
.text:00401241 mov ebp, esp
.text:00401243 sub esp, 8
.text:00401246 mov [esp+14h+var_14], 2
.text:0040124D call ds:__set_app_type
.text:00401253 call sub_401100
.text:00401258 nop
.text:00401259 lea esi, [esi+0]
.text:00401259 start endp
GCC更新 按照JimR的建议,我去看看sub_401100是什么,然后我跟着那个代码到另一个,这似乎是代码(我在这个假设中是否正确,如果sowhy GCC在主函数中有它的所有代码? ):
.text:00401100 sub_401100 proc near ; CODE XREF: .text:004010F1j
.text:00401100 ; start+13p ...
.text:00401100
.text:00401100 var_28 = dword ptr -28h
.text:00401100 var_24 = dword ptr -24h
.text:00401100 var_20 = dword ptr -20h
.text:00401100 var_1C = dword ptr -1Ch
.text:00401100 var_18 = dword ptr -18h
.text:00401100 var_C = dword ptr -0Ch
.text:00401100 var_8 = dword ptr -8
.text:00401100
.text:00401100 push ebp
.text:00401101 mov ebp, esp
.text:00401103 push ebx
.text:00401104 sub esp, 24h ; lpTopLevelExceptionFilter
.text:00401107 lea ebx, [ebp+var_8]
.text:0040110A mov [esp+28h+var_28], offset sub_401000
.text:00401111 call SetUnhandledExceptionFilter
.text:00401116 sub esp, 4 ; uExitCode
.text:00401119 call sub_4012E0
.text:0040111E mov [ebp+var_8], 0
.text:00401125 mov eax, offset dword_404000
.text:0040112A lea edx, [ebp+var_C]
.text:0040112D mov [esp+28h+var_18], ebx
.text:00401131 mov ecx, dword_402000
.text:00401137 mov [esp+28h+var_24], eax
.text:0040113B mov [esp+28h+var_20], edx
.text:0040113F mov [esp+28h+var_1C], ecx
.text:00401143 mov [esp+28h+var_28], offset dword_404004
.text:0040114A call __getmainargs
.text:0040114F mov eax, ds:dword_404010
.text:00401154 test eax, eax
.text:00401156 jz short loc_4011B0
.text:00401158 mov dword_402010, eax
.text:0040115D mov edx, ds:_iob
.text:00401163 test edx, edx
.text:00401165 jnz loc_4011F6
.text:004012E0 sub_4012E0 proc near ; CODE XREF: sub_401000+C6p
.text:004012E0 ; sub_401100+19p
.text:004012E0 push ebp
.text:004012E1 mov ebp, esp
.text:004012E3 fninit
.text:004012E5 pop ebp
.text:004012E6 retn
.text:004012E6 sub_4012E0 endp
答案 0 :(得分:3)
最有可能发生的事情是Borland在使用运行时库中存在的代码初始化所有代码后,从其启动代码调用main。
gcc代码看起来不像我的主要内容,但是像生成的代码一样调用main。在sub_401100处反汇编码,看看它是否与主过程相似。
答案 1 :(得分:3)
编译器输出预计会有所不同,有时对于同一来源会有很大的不同。与丰田和本田不同的方式相同。四个轮子和一些座椅确定,但是当你看细节时,它们会比同样的更多。
同样,具有不同编译器选项的相同编译器可以并且通常将为相同的源代码产生显着不同的输出。即使是看似简单的程序。
对于你的简单程序,它实际上什么都不做(代码不影响输入,输出,也不影响函数之外的任何东西),一个好的优化编译器只会导致main:返回一些随机数,因为你没有指定返回值。实际上它应该发出警告或错误。这是我比较编译器输出时所遇到的最大问题,它使得一些东西变得足够简单,可以看到它们正在做什么但复杂到足以使编译器做的不仅仅是预先计算答案并返回它。
在x86的情况下,我假设你在这里谈论的是,这些天被微编码,对于良好的代码与糟糕的代码,每个处理器系列他们改变了胆量以及过去的内容确实没有答案。快速是慢的,现在快速的旧处理器速度很慢。因此对于像gcc这样的编译器来说,随着新内核的不断发展,优化既可以是所有x86的通用优化,也可以是特定系列的特定功能(尽管进行了最大程度的优化,但仍会产生不同的代码)。
由于您对拆解的新兴趣,您将继续看到相似之处和不同之处,并找出可以编译相同代码的不同方式。即使对于琐碎的计划,也存在差异。我鼓励你尝试尽可能多的编译器。即使在gcc系列中,2.x,3.x,4.x以及构建它的不同方法也会产生不同的代码,这些代码可能被认为是同一个编译器。
良好与糟糕的输出是在旁观者的眼中。使用调试器的人会希望他们的代码可以操作并且他们的变量是可观察的(以书面的代码顺序)。这会产生非常大,笨重和慢的代码(特别是对于x86)。当你编译发布时,你最终会得到一个完全不同的程序,到目前为止你已经花了零时间进行调试。同样优化性能,您可能会冒险编译器优化您希望它做的事情(上面的例子,没有变量将被分配,没有代码可以逐步完成,即使是经过小幅优化)。或者更糟糕的是,你暴露了编译器中的错误,你的程序根本不起作用(这就是为什么-O3不鼓励gcc)。那和/或你发现C标准中的大量地方,其解释是实施定义的。
未经优化的代码更易于编译,因为它更加明显。在您的示例的情况下,期望是在堆栈上分配变量,设置某种堆栈指针排列,立即1最终写入该位置,堆栈清理并且函数返回。更难以让编译器出错并且更有可能使您的程序按预期工作。检测和删除死代码是优化和业务 这是风险的地方。通常风险值得奖励。但这取决于用户,美丽是在旁观者的眼中。
底线,简短回答。预计会有差异(甚至是戏剧性的差异)。默认编译选项因编译器而异。尝试编译/优化选项和不同的编译器,并继续反汇编您的程序,以便更好地了解您使用的语言和编译器。到目前为止,你走在正确的轨道上。对于borland输出,它检测到您的程序什么都不做,没有使用输入变量,没有使用返回变量,也没有与局部变量相关,也没有使用全局变量或函数资源的其他外部变量。整数a和立即赋值都是死代码,一个好的优化器基本上会删除/忽略这两行代码。因此,设置堆栈帧然后清理它不需要做,然后返回它是困扰的。 gcc看起来正在设置一个异常处理程序,即使它不需要,也可以开始优化或使用除main()之外的函数名称,并且你会看到不同的结果。
答案 2 :(得分:2)
首先,确保至少为gcc启用了-O2优化标志,否则根本就没有优化。
通过这个小例子,你没有真正测试优化,你看到程序初始化是如何工作的,例如gcc调用__set_app_type来通知窗口应用程序类型以及其他初始化。例如sub_401100为运行时注册atexit处理程序。 Borland可能事先调用运行时初始化,而gcc在main()中执行。
答案 3 :(得分:0)
看起来Borland编译器正在认识到你实际上从未对a
做任何事情而只是给你一个空主函数的等效程序集。
答案 4 :(得分:0)
以下是我在Gdb中从MinGW的gcc 4.5.1获得的main()
的反汇编(最后我添加了return 0
,因此GCC不会抱怨):
首先,当使用-O3优化编译程序时:
(gdb) set disassembly-flavor intel
(gdb) disassemble
Dump of assembler code for function main:
0x00401350 <+0>: push ebp
0x00401351 <+1>: mov ebp,esp
0x00401353 <+3>: and esp,0xfffffff0
0x00401356 <+6>: call 0x4018aa <__main>
=> 0x0040135b <+11>: xor eax,eax
0x0040135d <+13>: mov esp,ebp
0x0040135f <+15>: pop ebp
0x00401360 <+16>: ret
End of assembler dump.
没有优化:
(gdb) set disassembly-flavor intel
(gdb) disassemble
Dump of assembler code for function main:
0x00401350 <+0>: push ebp
0x00401351 <+1>: mov ebp,esp
0x00401353 <+3>: and esp,0xfffffff0
0x00401356 <+6>: sub esp,0x10
0x00401359 <+9>: call 0x4018aa <__main>
=> 0x0040135e <+14>: mov DWORD PTR [esp+0xc],0x1
0x00401366 <+22>: mov eax,0x0
0x0040136b <+27>: leave
0x0040136c <+28>: ret
End of assembler dump.
这些比Borland的例子稍微复杂一点,但并不过分。
注意,对0x4018aa
的调用是对库/编译器提供的函数的调用,以构造C ++对象。以下是一些GCC工具链文档的片段:
对构造函数的实际调用是由一个名为__main的子程序执行的,该子程序在main主体的开头被调用(自动)(提供的main是用GNU CC编译的)。即使在编译C代码时,调用__main也是必要的,以允许将C和C ++目标代码链接在一起。 (如果使用'-nostdlib',则会得到一个未解析的__main引用,因为它是在标准GCC库中定义的。在编译器命令行的末尾包含'-lgcc'以解析此引用。)
我不确定IDA Pro在您的示例中显示的是什么。 IDA Pro将其显示的内容标记为start
而不是main
,因此我猜测JimR's answer是正确的 - 它可能是运行时的初始化(可能是.exe标头中描述的入口点 - 它不是main()
,而是运行时初始化入口点。)
IDA Pro是否了解gcc的调试符号?您是否使用-g
选项进行编译以生成调试符号?
答案 5 :(得分:0)
这里的差异很少是在编译代码中,而是在反汇编程序中向您显示的内容。 您可能认为 main 是程序中唯一的功能,但事实并非如此。事实上你的程序是这样的:
void start()
{
... some initialization code here
int result = main();
... some deinitialization code here
ExitProcess(result);
}
IDA Pro了解Borland的工作原理,因此它可以直接导航到您的 main ,但它不知道gcc是如何工作的,因此它会向您显示程序的真正切入点。您可以在Borland ASM中看到从其他一些函数调用 main 。在GCC ASM中,您可以通过所有 sub_40xxx 来查找主要