GCC和Borland的反汇编C代码有什么区别?

发布于 2024-10-06 05:21:05 字数 4750 浏览 8 评论 0原文

最近,我对反汇编 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

画尸师 2024-10-13 05:21:05

对于同一源,编译器输出预计会有所不同,有时甚至会显着不同。就像丰田和本田不同一样。当然有四个轮子和一些座位,但当你看细节时,会发现更多的不同。

同样,具有不同编译器选项的同一编译器可以并且经常会为相同的源代码产生截然不同的输出。即使对于看似简单的程序也是如此。

对于您的简单程序,它实际上不执行任何操作(代码不影响输入,也不影响输出,也不影响函数之外的任何内容),一个好的优化编译器将只产生 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.

夏见 2024-10-13 05:21:05

这里最有可能发生的是,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.

柒夜笙歌凉 2024-10-13 05:21:05

首先,确保您至少启用了 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().

深海少女心 2024-10-13 05:21:05

这是我从 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 的答案是正确的 - 这可能是运行时的初始化(可能是 .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 a return 0 at the end so GCC wouldn't complain):

First, when the program is compiled with -O3 optimization:

(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.

And with no optimizations:

(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.

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:

The actual calls to the constructors are carried out by a subroutine called __main, which is called (automatically) at the beginning of the body of main (provided main was compiled with GNU CC). Calling __main is necessary, even when compiling C code, to allow linking C and C++ object code together. (If you use '-nostdlib', you get an unresolved reference to __main, since it's defined in the standard GCC library. Include '-lgcc' at the end of your compiler command line to resolve this reference.)

I'm not sure what exactly IDA Pro is showing in your examples. IDA Pro labels what it's showing as start not main 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 not main(), 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?

迷爱 2024-10-13 05:21:05

看起来 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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文