GCC和Borland的反汇编C代码有什么区别?
最近,我对反汇编 C 代码(非常简单的 C 代码)感兴趣,并遵循了使用 Borland C++ Compiler v 5.5 的教程(可以很好地编译 C 代码),一切正常。然后我决定尝试自己的 C 代码并在 Dev C++(使用 gcc)中编译它们。在IDA Pro中打开它后我大吃一惊,gcc的asm和Borland的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(更新如下)
.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 是什么,然后我按照该代码找到了另一个代码,这似乎是代码(我的假设正确吗?如果是的话,为什么 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
Recently I have gotten interested into dis-assembling C code (very simple C code) and followed a tutorial that used Borland C++ Compiler v 5.5 (compiles C code just fine) and everything worked. Then I decided to try my own c code and compiled them in Dev C++ (which uses gcc). Upon opening it in IDA Pro I got a surprise, the asm of gcc was really different compared to Borland's. I expected some difference but the C code was EXTREMELY simple, so is it just that gcc doesn't optimize as much or is it that they use different default compiler settings?
The C Code
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 (UPDATED 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 Update
Upon following the suggestion of JimR I went to see what sub_401100 is and then I followed that code to another and this seems to be the code (Am I correct in that assumption and if sowhy does GCC have all of its code in the main function?):
.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
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
对于同一源,编译器输出预计会有所不同,有时甚至会显着不同。就像丰田和本田不同一样。当然有四个轮子和一些座位,但当你看细节时,会发现更多的不同。
同样,具有不同编译器选项的同一编译器可以并且经常会为相同的源代码产生截然不同的输出。即使对于看似简单的程序也是如此。
对于您的简单程序,它实际上不执行任何操作(代码不影响输入,也不影响输出,也不影响函数之外的任何内容),一个好的优化编译器将只产生 main: 返回一些随机数因为你没有指定返回值。实际上它应该给出警告或错误。当我比较编译器输出时,这是我遇到的最大问题是使一些东西足够简单以查看它们在做什么,但又足够复杂以至于编译器不仅仅预先计算答案并返回它。
就 x86 而言,我认为这就是您在这里谈论的内容,现在微编码确实没有好代码与坏代码的答案,每个处理器系列都改变了内部结构,以前快的变成了慢现在快的东西在旧处理器上却很慢。因此,对于像 gcc 这样随着新内核不断发展的编译器,优化可以是对所有 x86 通用的,也可以是特定于特定系列的(尽管进行了最大优化,但仍会产生不同的代码)。
随着您对反汇编的新兴趣,您将继续看到相似点和差异,并找出可以编译相同代码的不同方式。即使对于微不足道的程序,也会存在差异。我鼓励您尝试尽可能多的编译器。即使在 gcc 系列 2.x、3.x、4.x 中,不同的构建方式也会导致可能被认为是相同编译器的不同代码。
输出的好坏是仁者见仁智者见智的。使用调试器的人们会希望他们的代码是可步进的并且他们的变量是可观察的(按书面代码顺序)。这会导致代码变得非常大、笨重且缓慢(特别是对于 x86)。当您编译发布时,您最终会得到一个完全不同的程序,到目前为止您花费了零时间调试。另外,针对性能进行优化时,您还需要承担编译器优化您想要它执行的操作的风险(上面的示例,不会分配任何变量,也不会执行任何代码,即使进行了较小的优化)。或者更糟糕的是,您暴露了编译器中的错误,并且您的程序根本无法工作(这就是为什么 gcc 不鼓励使用 -O3 的原因)。那和/或你会发现 C 标准中的大量地方其解释是实现定义的。
未优化的代码更容易编译,因为它更明显。在您的示例中,期望是在堆栈上分配一个变量,设置某种堆栈指针排列,立即数 1 最终写入该位置,清理堆栈并返回函数。编译器更难出错,并且您的程序更有可能按您的预期运行。检测和删除死代码是优化和
这就是风险所在。通常,风险是值得回报的。但这取决于用户,情人眼里出西施。
底线,简短的回答。差异是预料之中的(甚至是巨大的差异)。默认编译选项因编译器而异。尝试编译/优化选项和不同的编译器,并继续反汇编您的程序,以便更好地了解您使用的语言和编译器。到目前为止,您走在正确的轨道上。在 borland 输出的情况下,它检测到您的程序什么都不做,没有使用输入变量,没有使用返回变量,也没有与局部变量相关,并且没有使用全局变量或其他外部函数资源。整数 a 和立即数的赋值是死代码,好的优化器基本上会删除/忽略这两行代码。因此,它费心去设置一个堆栈帧,然后清理它不需要做的事情,然后返回。 gcc 看起来正在设置一个异常处理程序,尽管它不需要,但它完全没问题,开始优化或使用 main() 以外的函数名称,您应该会看到不同的结果。
Compiler output is expected to be different, sometimes dramatically different for the same source. In the same way that a toyota and a honda are different. Four wheels and some seats sure, but more different than the same when you look at the details.
Likewise the same compiler with different compiler options can and often will produce dramatically different output for the same source code. Even for what appears to be simple programs.
In the case of your simple program, which actually does not do anything (code does not affect the input, nor output, nor anything outside the function), a good optimized compiler will result in nothing but main: with a return of some random number since you didnt specify the return value. Actually it should give a warning or error. This is the biggest problem I have when I compare compiler output is making something simple enough to see what they are doing but something complicated enough that the compiler does more than just pre-compute the answer and return it.
In the case of x86, which I assume is what you are talking about here, being microcoded these days there is really no answer for good code vs bad code, each family of processor they change the guts around and what used to be fast is slow and what is now fast is slow on the old processor. So for compilers like gcc that have continued to evolve with the new cores, the optimization can be both generic to all x86es or specific to a particular family (resulting in different code despite max optimization).
With your new interest in disassembling, you will continue to see the similarities and differences and find out just how many different ways the same code can be compiled. the differences are expected, even for trivial programs. And I encourage you to try as many compilers as you can. Even in the gcc family 2.x, 3.x, 4.x and the different ways to build it will result in different code for what might be though thought of as the same compiler.
Good vs bad output is in the eyes of the beholder. Folks that use debuggers will want their code steppable and their variables watchable (in written code order). This makes for very big, bulky, and slow code (particularly for x86). And when you compile for release you end up with a completely different program which you have so far spent zero time debugging. Also optimizing for performance you take a risk of the compiler optimizing out something you wanted it to do (your example above, no variable will be allocated, no code to step through, even with minor optimization). Or worse, you expose the bugs in the compiler and your program simply doesnt work (this is why -O3 is discouraged for gcc). That and/or you find out the large number of places in the C standard whose interpretation is implementation defined.
Unoptimized code is easier to compile, as it is a bit more obvious. In the case of your example the expectation is a variable is allocated on the stack, some sort of stack pointer arrangement set up, the immediate 1 is eventually written to that location, stack cleaned up and function returns. Harder for compilers to get wrong and more likely that your program works as you intended. Detecting and removing dead code is the business of optimization and
that is where it gets risky. Often the risk is worth the reward. But that depends on the user, beauty is in the eye of the beholder.
Bottom line, short answer. Differences are expected (even dramatic differences). Default compile options vary from compiler to compiler. Experiment with the compile/optimization options and different compilers and continue to disassemble your programs in order to gain a better education about the language and the compilers you use. You are on the right track so far. In the case of the borland output, it detected that your program does nothing, no input variables are used, no return variables are used, nor related to the local variables, and no global variables or other external to the function resources are used. The integer a and the assignment of an immediate are dead code, a good optimizer will essentially remove/ignore both lines of code. So it bothered to setup a stack frame then clean it up which it didnt need to do, then returned. gcc looks to be setting up an exception handler which is perfectly fine even though it doesnt need to, start optimizing or use a function name other than main() and you should see different results.
这里最有可能发生的是,Borland 在使用运行时库中存在的代码初始化所有内容后,从其启动代码中调用 main 。
gcc 代码对我来说不像 main,而是像调用 main 的生成代码。反汇编 sub_401100 处的代码,看看它是否看起来像您的主过程。
What is most likely happening here is that Borland calls main from its start up code after initializing everything with code present in their run time lib.
The gcc code does not look like main to me, but like generated code that calls main. Disassemble the code at sub_401100 and see if it looks like your main proc.
首先,确保您至少启用了 gcc 的 -O2 优化标志,否则您根本得不到优化。
通过这个小例子,您并不是真正测试优化,而是看到程序初始化是如何工作的,例如 gcc 调用 __set_app_type 通知窗口应用程序类型以及其他初始化。例如 sub_401100 为运行时注册 atexit 处理程序。 Borland 可能会预先调用运行时初始化,而 gcc 在 main() 中进行。
First of all, make sure you have at least enabled the -O2 optimization flag to gcc, otherwise you get no optimization at all.
With this little example, you arn't really testing optimization, you're seeing how program initialization works, e.g. gcc calls __set_app_type to inform windows of the application type, as well as other initialization. e.g. sub_401100 registers atexit handlers for the runtime. Borland might call the runtime initialization beforehand, while gcc does it within main().
这是我从 gdb 中的 MinGW 的 gcc 4.5.1 获得的
main()
反汇编(我在末尾添加了return 0
这样 GCC 就不会抱怨): ,当使用 -O3 优化编译程序时:
和不使用优化时:
这些比 Borland 的示例稍微复杂一些,但也不过分。
请注意,对
0x4018aa
的调用是对库/编译器提供的函数的调用,用于构造 C++ 对象。以下是一些 GCC 工具链文档的片段:我不确定IDA Pro 在您的示例中到底显示了什么。 IDA Pro 将其显示的内容标记为
start
而不是main
所以我猜 JimR 的答案是正确的 - 这可能是运行时的初始化(可能是 .exe 标头中描述的入口点 - 其中不是main()
,而是运行时初始化入口点)。IDA Pro 能理解 gcc 的调试符号吗?您是否使用
-g
选项进行编译以便生成调试符号?Here's the disassembly of
main()
that I get from MinGW's gcc 4.5.1 in gdb (I added areturn 0
at the end so GCC wouldn't complain):First, when the program is compiled with -O3 optimization:
And with no optimizations:
These are a little more complex than Borland's example, but not excessively.
Note, the calls to
0x4018aa
are calls to a library/compiler supplied function to construct C++ objects. Here's a snippet from some GCC toolchain docs:I'm not sure what exactly IDA Pro is showing in your examples. IDA Pro labels what it's showing as
start
notmain
so I'd guess that JimR's answer is right - it's probably the runtime's initialization (perhaps the entry point as described in the .exe header - which is notmain()
, but the runtime initialization entry point).Does IDA Pro understand gcc's debug symbols? Did you compile with the
-g
option so the debug symbols are generated?看起来 Borland 编译器认识到您实际上从未使用
a
执行任何操作,而只是为您提供空 main 函数的等效程序集。It looks like the Borland compiler is recognizing that you never actually do anything with
a
and is just giving you the equivalent assembly for an empty main function.