【问题标题】:Differences in dis-assembled C code of GCC and Borland?GCC和Borland反汇编C代码的区别?
【发布时间】:2011-05-20 07:53:39
【问题描述】:

最近我对反汇编 C 代码(非常简单的 C 代码)产生了兴趣,并遵循了一个使用 Borland C++ Compiler v 5.5 的教程(可以很好地编译 C 代码),一切正常。然后我决定尝试我自己的 c 代码并在 Dev C++(使用 gcc)中编译它们。在 IDA Pro 中打开它时,我感到很惊讶,gcc 的 asm 与 Borland 的完全不同。我预计会有一些差异,但 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 的所有代码都在 main 函数中? ):

.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

【问题讨论】:

    标签: c assembly disassembly


    【解决方案1】:

    这里的区别主要不在于编译的代码,而在于反汇编程序向您显示的内容。 您可能认为 ma​​in 是程序中唯一的函数,但事实并非如此。事实上你的程序是这样的:

    void start()
    {
        ... some initialization code here
        int result = main();
        ... some deinitialization code here
        ExitProcess(result);
    }
    

    IDA Pro 知道 Borland 的工作原理,因此它可以直接导航到您的 ma​​in,但它不知道 gcc 的工作原理,因此它向您显示程序的真正入口点。您可以在 Borland ASM 中看到 ma​​in 是从某个其他函数调用的。在 GCC ASM 中,您可以通过所有这些 sub_40xxx 找到您的 ma​​in

    【讨论】:

      【解决方案2】:

      这是我在 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 工具链文档的 sn-p:

      对构造函数的实际调用由一个名为 __main 的子例程执行,该子例程在 main 主体的开头(自动)调用(前提是 main 是用 GNU CC 编译的)。调用 __main 是必要的,即使在编译 C 代码时,也可以将 C 和 C++ 目标代码链接在一起。 (如果你使用“-nostdlib”,你会得到一个未解析的 __main 引用,因为它是在标准 GCC 库中定义的。在编译器命令行的末尾包含“-lgcc”来解析这个引用。)

      我不确定 IDA Pro 在您的示例中究竟显示了什么。 IDA Pro 将它显示的内容标记为start 而不是main 所以我猜JimR's answer 是正确的——它可能是运行时的初始化(也许是.exe 标头中描述的入口点——不是main() ,而是运行时初始化入口点)。

      IDA Pro 是否理解 gcc 的调试符号?您是否使用-g 选项进行编译以便生成调试符号?

      【讨论】:

        【解决方案3】:

        编译器的输出预计会有所不同,有时对于相同的源会有很大的不同。就像丰田和本田不同一样。肯定有四个轮子和一些座位,但是当您查看细节时,会有更多不同。

        同样,具有不同编译器选项的相同编译器可以并且通常会为相同的源代码产生显着不同的输出。即使是看似简单的程序。

        对于你的简单程序,它实际上什么都不做(代码不影响输入,也不影响输出,也不影响函数之外的任何东西),一个好的优化编译器只会产生 main: 并返回一些随机数,因为您没有指定返回值。实际上它应该给出警告或错误。当我比较编译器输出时,我遇到的最大问题是让一些事情变得足够简单以查看它们在做什么,但事情又足够复杂以至于编译器所做的不仅仅是预先计算答案并返回它。

        在 x86 的情况下,我假设这就是您在此处谈论的内容,这些天被微编码确实没有好代码与坏代码的答案,每个处理器系列都会改变周围的胆量以及过去是什么快很慢,现在的快在旧处理器上很慢。因此,对于像 gcc 这样随着新内核不断发展的编译器,优化可以是所有 x86es 通用的,也可以是特定系列的(尽管进行了最大优化,但会产生不同的代码)。

        随着您对反汇编的新兴趣,您将继续看到相似之处和不同之处,并找出可以编译相同代码的多种不同方式。即使对于微不足道的程序,也会出现差异。我鼓励您尝试尽可能多的编译器。即使在 gcc 系列 2.x、3.x、4.x 和不同的构建方式中,也会导致可能被认为是同一个编译器的不同代码。

        好与坏的输出在旁观者的眼中。使用调试器的人会希望他们的代码是可步进的并且他们的变量是可观察的(以书面代码顺序)。这导致代码非常大、笨重且速度慢(尤其是对于 x86)。当你编译发布时,你最终会得到一个完全不同的程序,到目前为止你已经花了零时间进行调试。同样优化性能,你冒着编译器优化你想要它做的事情的风险(你上面的例子,没有变量将被分配,没有代码可以单步执行,即使是轻微的优化)。或者更糟的是,您暴露了编译器中的错误,而您的程序根本无法工作(这就是不鼓励使用 gcc 的 -O3 的原因)。那和/或您会发现 C 标准中的大量位置,其解释是实现定义的。

        未优化的代码更容易编译,因为它更明显。在您的示例中,期望是在堆栈上分配一个变量,设置某种堆栈指针排列,立即数 1 最终写入该位置,清理堆栈并返回函数。编译器更难出错,您的程序更有可能按预期工作。检测和删除死代码是优化的业务 这就是风险所在。通常,风险是值得的。但这取决于用户,美丽在旁观者的眼中。

        底线,简短的回答。预计会有差异(甚至是巨大的差异)。默认编译选项因编译器而异。尝试使用编译/优化选项和不同的编译器,并继续反汇编您的程序,以便更好地了解您使用的语言和编译器。到目前为止,您走在正确的轨道上。在borland输出的情况下,它检测到你的程序什么都不做,没有使用输入变量,没有使用返回变量,也没有与局部变量相关,也没有使用全局变量或函数外部的其他资源。整数 a 和立即数的赋值是死代码,一个好的优化器基本上会删除/忽略这两行代码。所以它费心设置一个堆栈帧然后清理它不需要做的,然后返回。 gcc 看起来正在设置一个非常好的异常处理程序,即使它不需要开始优化或使用除 main() 之外的函数名称,您应该会看到不同的结果。

        【讨论】:

        • 哇,哇,谢谢,这对您有很大帮助。 :-) 我会让这个问题保持开放,但到目前为止你得到了支票和至少一个赞成票:-)。
        • 不要急于核对答案,不管怎样,给它几天。毫无疑问,还有其他人提供了值得检查的答案,请将其打开,以便他们有机会。
        【解决方案4】:

        Borland 编译器似乎认识到您实际上从未对 a 执行任何操作,而只是为您提供了空主函数的等效程序集。

        【讨论】:

        • 是的,这就是我所遵循的教程所说的,但是为什么 gcc 不这样做呢?
        【解决方案5】:

        首先,确保你至少为 gcc 启用了 -O2 优化标志,否则你根本得不到优化。

        通过这个小例子,您并没有真正测试优化,您看到的是程序初始化是如何工作的,例如gcc 调用__set_app_type 通知窗口应用程序类型,以及其他初始化。例如sub_401100 为运行时注册 atexit 处理程序。 Borland 可能会事先调用运行时初始化,而 gcc 在 main() 中进行。

        【讨论】:

        • 我确实启用了 O2,它并没有改变上面的任何反汇编输出。
        • 因为该代码只是程序启动/拆卸,无论您是创建穿梭引导系统还是打印hello world,它都将保持不变
        【解决方案6】:

        这里最有可能发生的是,在使用运行时库中存在的代码初始化所有内容之后,Borland 从其启动代码调用 main。

        gcc 代码在我看来并不像 main,而是像调用 main 的生成代码。反汇编 sub_401100 的代码,看看它是否看起来像你的主进程。

        【讨论】:

        • 看起来 GCC 可能完全跳过了调用 main(),因为它没有做任何“可观察”的事情。所以可能gcc的main()比Borland的还要简单。
        • @Michael Burr:有可能......我发现 gcc 在某些事情上非常聪明。那个 Borland 优化器可能已经有 10 年历史了,最近没有更新:)
        • @Zimm3r: sub_401100 在大多数情况下只是更多的 clib 启动代码。如果我和其他 cmets 不清楚,Borland 会将他们的启动代码放在他们的 clib 中。在我看来,gcc 会为每次编译生成启动代码。如果您有 Borland clib 的源代码,请查看 startup.asm 或 startup.s。 (我认为这就是文件名……已经有好几年了。)
        猜你喜欢
        • 2014-03-24
        • 1970-01-01
        • 2011-05-03
        • 2021-04-14
        • 2012-01-05
        • 2014-06-21
        • 2013-01-22
        • 1970-01-01
        • 2011-03-26
        相关资源
        最近更新 更多