使用GCC生成可读的汇编代码?

315

我想知道如何使用GCC来将我的C源文件转换成机器码的助记符版本,以便我可以看到我的代码被编译成了什么。在Java中可以这样做,但我没有找到使用GCC的方法。

我正在尝试用汇编语言重写一个C方法,查看GCC是如何实现的会很有帮助。


31
请注意,“bytecode”通常指虚拟机(如JVM或.NET的CLR)使用的代码。GCC的输出最好称为“机器码”、“机器语言”或“汇编语言”。 - Javier
2
我使用godbolt添加了一个答案,因为它是一个非常强大的工具,可以快速实验不同选项对代码生成的影响。 - Shafik Yaghmour
https://dev59.com/FHVC5IYBdhLWcg3w9GE9#19083877 - phuclv
可能是如何从gcc的C/C++源代码中获取汇编器输出?的重复问题。 - Ciro Santilli OurBigBook.com
1
有关使汇编输出易读的更多提示,请参见:如何从GCC/clang汇编输出中去除“噪音”? - Peter Cordes
1
在这里回答:https://dev59.com/FHVC5IYBdhLWcg3w9GE9 使用gcc(或g ++)的-S选项。 - knowledge_is_power
11个回答

395
如果您在编译时添加了调试符号(即使您也使用了-O31),可以使用objdump -S生成与C源代码交错的更易读的反汇编。
>objdump --help
[...]
-S, --source             Intermix source code with disassembly
-l, --line-numbers       Include line numbers and filenames in output

objdump -drwC -Mintel 是一个不错的工具:

  • -r 显示重定位中的符号名称(因此您将在下面的call指令中看到puts
  • -R 显示动态链接重定位/符号名称(在共享库上很有用)
  • -C 解开 C++ 符号名称的编码
  • -w 是“宽”模式:它不会将机器码字节换行
  • -Mintel:使用 GAS/binutils MASM 类似的 .intel_syntax noprefix 语法,而不是 AT&T 语法
  • -S:将源代码行与反汇编交替显示。

您可以在~/.bashrc中添加类似于alias disas="objdump -drwCS -Mintel"的内容。如果不在 x86 上,或者喜欢 AT&T 语法,则省略-Mintel


例子:

> gcc -g -c test.c
> objdump -d -M intel -S test.o

test.o:     file format elf32-i386


Disassembly of section .text:

00000000 <main>:
#include <stdio.h>

int main(void)
{
   0:   55                      push   ebp
   1:   89 e5                   mov    ebp,esp
   3:   83 e4 f0                and    esp,0xfffffff0
   6:   83 ec 10                sub    esp,0x10
    puts("test");
   9:   c7 04 24 00 00 00 00    mov    DWORD PTR [esp],0x0
  10:   e8 fc ff ff ff          call   11 <main+0x11>

    return 0;
  15:   b8 00 00 00 00          mov    eax,0x0
}
  1a:   c9                      leave  
  1b:   c3                      ret

请注意,此处并未使用-r选项,因此call rel32=-4未注释为puts符号名称。看起来像是一个破碎的call,跳入主调指令中间。请记住,在调用编码中的rel32位移仅用作占位符,直到链接器填充实际偏移量(在这种情况下,到PLT存根,除非您静态链接libc)。
脚注1:交错源代码可能会很混乱,并且在优化构建中没有太多帮助;对此,请考虑使用https://godbolt.org/或其他可视化方法,以确定哪些指令与哪些源代码行相对应。在优化代码中,并不总是有一行源代码可以解释一条汇编指令,但调试信息将为每个汇编指令选择一行源代码。

3
有没有一个开关只捕获英特尔指令? - James
3
所有这些都是英特尔指令,因为它们在英特尔处理器上运行:D。 - toto
14
@toto,我认为他的意思是使用英特尔语法而不是AT&T语法。 - Amok
8
可以使用gcc的开关序列“-Wa,-adhln -g”来省略中间目标文件。这假设汇编程序是gas,但并非总是如此。 - Marc Butler
8
@James,是的,请提供"-Mintel"。 - fuz
2
Mac用户可以使用otool -tV命令。 - transang

128
如果你给GCC加上-fverbose-asm标志,它会在生成的汇编代码中添加额外的注释信息,使其更易读。添加的注释包括:编译器版本和命令行选项的信息,与汇编指令相关联的源代码行,以 FILENAME:LINENUMBER:CONTENT OF LINE 的形式呈现,以及关于哪些高级表达式对应于各种汇编指令操作数的提示。

但是,如果我不使用 objdump -drwCS -Mintel 中的所有开关,那么我该如何在 objdump 中使用类似于 verbose 的东西呢?这样我就可以在汇编代码中添加注释,就像 gcc 中的 -fverbose-asm 一样了。 - Herdsman
5
@Herdsman:你做不到。-fverbose-asm 添加的额外信息是以注释形式出现在输出的汇编代码中,而不是指令,不会将任何额外内容放入 .o 文件中。 这些信息在汇编时全部被丢弃了。 请查看编译器生成的汇编输出,而不是反汇编输出,例如在 https://godbolt.org/ 上,你可以通过鼠标悬停和相应源代码/汇编代码行的颜色高亮匹配它们。 如何从GCC / clang汇编输出中删除“噪音”? - Peter Cordes

82
使用-S选项(注意:大写S)来调用GCC,它将生成汇编代码并保存到一个扩展名为.s的文件中。例如,下面的命令: gcc -O2 -S foo.c 会把生成的汇编代码保存到文件foo.s中。
摘自http://www.delorie.com/djgpp/v2faq/faq8_20.html(但删除了错误的-c)。

37
不应该混合使用 -c 和 -S 参数,只能选择其中一个。在这种情况下,其中一个参数将覆盖另一个,可能取决于它们被使用的顺序。 - Adam Rosenfield
4
@AdamRosenfield 是否有关于“不应该混合使用-c和-S”的参考?如果属实,我们可能需要提醒作者并进行编辑。 - Tony
6
@Tony: 在https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html#Overall-Options中提到:“您可以使用... 其中一个 选项-c、-S或-E来指定gcc停止的位置。” - Nate Eldredge
2
如果您想要所有的中间输出,请使用 gcc -march=native -O3 -save-temps。您仍然可以使用 -c 停止在创建目标文件而不是尝试链接,或者其他操作。 - Peter Cordes
2
@StéphaneGourichon:没错;更多是用于调试/创建编译器错误报告,而不是这种情况。我从来不使用-save-temps来查看某些源代码编译为汇编的方式,而是使用-masm=intel -S -o- | less,反汇编.o或可执行文件,或将其放在https://godbolt.org/上。 - Peter Cordes
显示剩余2条评论

54

在基于x86系统上使用-S开关对GCC进行编译,会默认生成AT&T语法的转储文件。可以使用-masm=att开关指定生成AT&T语法的转储文件,如下:

gcc -S -masm=att code.c

如果您想以Intel语法生成转储文件,可以使用-masm=intel开关,如下所示:

gcc -S -masm=intel code.c

两者都将code.c的转储输出成各自不同的语法,并分别输出到文件code.s中。

如果要使用objdump产生类似的效果,您需要使用--disassembler-options=intel/att开关。下面是一个例子(带有代码转储以说明语法上的差异):

 $ objdump -d --disassembler-options=att code.c

 080483c4 <main>:
 80483c4:   8d 4c 24 04             lea    0x4(%esp),%ecx
 80483c8:   83 e4 f0                and    $0xfffffff0,%esp
 80483cb:   ff 71 fc                pushl  -0x4(%ecx)
 80483ce:   55                      push   %ebp
 80483cf:   89 e5                   mov    %esp,%ebp
 80483d1:   51                      push   %ecx
 80483d2:   83 ec 04                sub    $0x4,%esp
 80483d5:   c7 04 24 b0 84 04 08    movl   $0x80484b0,(%esp)
 80483dc:   e8 13 ff ff ff          call   80482f4 <puts@plt>
 80483e1:   b8 00 00 00 00          mov    $0x0,%eax
 80483e6:   83 c4 04                add    $0x4,%esp 
 80483e9:   59                      pop    %ecx
 80483ea:   5d                      pop    %ebp
 80483eb:   8d 61 fc                lea    -0x4(%ecx),%esp
 80483ee:   c3                      ret
 80483ef:   90                      nop

并且

$ objdump -d --disassembler-options=intel code.c

 080483c4 <main>:
 80483c4:   8d 4c 24 04             lea    ecx,[esp+0x4]
 80483c8:   83 e4 f0                and    esp,0xfffffff0
 80483cb:   ff 71 fc                push   DWORD PTR [ecx-0x4]
 80483ce:   55                      push   ebp
 80483cf:   89 e5                   mov    ebp,esp
 80483d1:   51                      push   ecx
 80483d2:   83 ec 04                sub    esp,0x4
 80483d5:   c7 04 24 b0 84 04 08    mov    DWORD PTR [esp],0x80484b0
 80483dc:   e8 13 ff ff ff          call   80482f4 <puts@plt>
 80483e1:   b8 00 00 00 00          mov    eax,0x0
 80483e6:   83 c4 04                add    esp,0x4
 80483e9:   59                      pop    ecx
 80483ea:   5d                      pop    ebp
 80483eb:   8d 61 fc                lea    esp,[ecx-0x4]
 80483ee:   c3                      ret    
 80483ef:   90                      nop

什么鬼... gcc -S -masm=intel test.c 对我来说并没有完全起作用,我得到了一些Intel和AT&T语法的混合体,例如:mov %rax,QWORD PTR -24[%rbp],而不是这个:movq -24(%rbp),%rax - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
1
不错的技巧。需要注意的是,当执行.o和ASM文件的并行输出时,即通过-Wa,-ahls -o yourfile.o yourfile.cpp>yourfile.asm,此方法同样适用。 - underscore_d
可以使用-M选项,它与--disassembler-options相同,但更短,例如objdump -d -M intel a.out | less -N - Eric

35

godbolt 是一个非常有用的工具,它们只列出了 C++ 编译器,但是你可以使用 -x c 标志来让它将代码视为 C 代码进行处理。然后,它会生成你的代码的汇编清单,并且你可以使用 Colourise 选项生成彩色条,以视觉上指示哪些源代码映射到生成的汇编代码。例如下面的代码:

#include <stdio.h>

void func()
{
  printf( "hello world\n" ) ;
}

使用以下命令行:

-x c -std=c99 -O3

同时使用 Colourise 会生成以下内容:

enter image description here


了解godbolt过滤器的工作原理会很不错:.LC0、.text、//和Intel。Intel很容易,只需使用-masm=intel,但其他过滤器呢? - Z boson
我想这里已经解释了 https://dev59.com/n1kT5IYBdhLWcg3wefUn#38552509 - Z boson
godbolt 支持 C 语言(以及 Rust、D、Pascal 等许多其他语言)。只是 C 编译器较少,因此最好仍然使用带有 -x c 的 C++ 编译器。 - phuclv
为什么源代码和汇编代码中的字符串不同?换行符已在末尾被剥离。 - Героям слава

26
你尝试过执行 gcc -S -fverbose-asm -O source.c 命令,然后查看生成的汇编文件source.s吗?
生成的汇编代码会被放入source.s文件中(你可以用-oassembler-filename来覆盖默认文件名);-fverbose-asm选项要求编译器生成一些汇编注释来“解释”生成的汇编代码。而-O选项是让编译器进行轻微的优化(使用-O2-O3能够进行更多优化)。
如果你想了解gcc正在做什么,可以尝试使用-fdump-tree-all选项,但需谨慎:你将得到数百个转储文件。
顺便提一下,GCC可以通过插件MELT(一种高级领域特定语言,用于扩展GCC)来扩展。我在2017年已经放弃了MELT。

可以提及输出将在source.s中,因为许多人期望在控制台上输出打印。 - RubenLaguna
1
@ecerulm: -S -o- 输出到标准输出。如果要使用NASM/YASM语法,-masm=intel 是有帮助的(但是它使用的是qword ptr [mem],而不仅仅是qword,所以更像Intel/MASM而不是NASM/YASM)。http://gcc.godbolt.org/ 在整理输出时做得很好:可选地剥离仅注释行、未使用的标签和汇编指令。 - Peter Cordes
3
忘了提一下:如果你正在寻找“与源代码类似但没有在每个源代码行后进行存储/重新加载的噪声”,那么-Og-O1更好。它的意思是“为调试优化”,可以生成汇编代码,而不包含太多复杂/难以跟踪的优化内容,并且会按照源代码的意思执行操作。这个选项自gcc4.8之后就已经可用了,但clang 3.7仍然没有这个选项。我不知道他们是否决定放弃它。 - Peter Cordes
更新:clang现在支持-Og。此外,请参见如何从GCC / clang汇编输出中删除“噪音”?,了解有关编写编译为有趣汇编代码的测试函数以及查看GCC / clang选项的更多信息,以直接查看它们的汇编输出,而不是反汇编。我不知道为什么这个问题的答案大多是关于反汇编而不是GCC汇编输出。 - Peter Cordes

17
你可以使用像objdump一样的gdb工具来完成这个操作。
这段摘录来自http://sources.redhat.com/gdb/current/onlinedocs/gdb_9.html#SEC64
下面是一个显示Intel x86混合源代码和汇编代码的示例:
  (gdb) disas /m main
Dump of assembler code for function main:
5       {
0x08048330 :    push   %ebp
0x08048331 :    mov    %esp,%ebp
0x08048333 :    sub    $0x8,%esp
0x08048336 :    and    $0xfffffff0,%esp
0x08048339 :    sub    $0x10,%esp

6         printf ("Hello.\n");
0x0804833c :   movl   $0x8048440,(%esp)
0x08048343 :   call   0x8048284
7 return 0; 8 } 0x08048348 : mov $0x0,%eax 0x0804834d : leave 0x0804834e : ret
End of assembler dump.

1
存档链接:https://web.archive.org/web/20090412112833/http://sourceware.org:80/gdb/current/onlinedocs/gdb_9.html - vlad4378
而要将GDB的反汇编器切换为Intel语法,请使用“set disassembly-flavor intel”命令。 - Ruslan

13
使用-S(注意:大写S)开关调用GCC,它将把汇编代码输出到一个扩展名为.s的文件中。例如,执行以下命令:
gcc -O2 -S -c foo.c

6

我还没有尝试过gcc,但对于g++来说,下面的命令对我有效。

  • -g用于调试构建
  • -Wa,-adhln传递给汇编器以便与源代码一起列出
g++ -g -Wa,-adhln src.cpp

1
它也适用于gcc!-Wa,...是汇编器部分的命令行选项(在C / C ++编译后在gcc / g ++中执行)。它在内部调用(在Windows中为as.exe)。 请参见以下命令行以获取更多帮助:
as --help
- Hartmut Schorrig

2

对于RISC-V反汇编,这些标志很好用:

riscv64-unknown-elf-objdump -d -S -l --visualize-jumps --disassembler-color=color --inlines

-d:反汇编,最基本的标志

-S:混合源代码。注意:编译时必须使用-g标志

-l:行号

--visualize-jumps:花哨的箭头,没有太多用处,但为什么不试一下呢。有时会变得太杂乱,实际上使阅读源代码更加困难。引用自Peter Cordes的评论:--visualize-jumps=color也是一个选项,可以为不同的箭头使用不同的颜色

--disassembler-color=color:给反汇编添加一些颜色

--inlines:打印内联函数

可能有用:

-M numeric:使用数字寄存器名称而不是ABI名称,如果您正在进行CPU开发并且不知道ABI名称,这将非常有用

-M no-aliases:不要使用伪指令,如licall

示例:main.o

#include <stdio.h>
#include <stdint.h>

static inline void example_inline(const char* str) {
    for (int i = 0; str[i] != 0; i++)
        putchar(str[i]);
}

int main() {
    printf("Hello world");
    example_inline("Hello! I am inlined");

    return 0;
}

如果您想混合使用源代码,我建议使用-O0。如果使用-O2,则混合使用源代码会变得非常混乱。

命令:

riscv64-unknown-elf-gcc main.c -c -O0 -g
riscv64-unknown-elf-objdump -d -S -l --disassembler-color=color --inlines main.o

反汇编:

main.o:     file format elf64-littleriscv


Disassembly of section .text:

0000000000000000 <example_inline>:
example_inline():
/Users/cyao/test/main.c:4
#include <stdio.h>
#include <stdint.h>

static inline void example_inline(const char* str) {
   0:   7179                    addi    sp,sp,-48
   2:   f406                    sd  ra,40(sp)
   4:   f022                    sd  s0,32(sp)
   6:   1800                    addi    s0,sp,48
   8:   fca43c23                sd  a0,-40(s0)

000000000000000c <.LBB2>:
/Users/cyao/test/main.c:5
    for (int i = 0; str[i] != 0; i++)
   c:   fe042623                sw  zero,-20(s0)
  10:   a01d                    j   36 <.L2>

0000000000000012 <.L3>:
/Users/cyao/test/main.c:6 (discriminator 3)
        putchar(str[i]);
  12:   fec42783                lw  a5,-20(s0)
  16:   fd843703                ld  a4,-40(s0)
  1a:   97ba                    add a5,a5,a4
  1c:   0007c783                lbu a5,0(a5)
  20:   2781                    sext.w  a5,a5
  22:   853e                    mv  a0,a5
  24:   00000097                auipc   ra,0x0
  28:   000080e7                jalr    ra # 24 <.L3+0x12>
/Users/cyao/test/main.c:5 (discriminator 3)
    for (int i = 0; str[i] != 0; i++)
  2c:   fec42783                lw  a5,-20(s0)
  30:   2785                    addiw   a5,a5,1
  32:   fef42623                sw  a5,-20(s0)

0000000000000036 <.L2>:
/Users/cyao/test/main.c:5 (discriminator 1)
  36:   fec42783                lw  a5,-20(s0)
  3a:   fd843703                ld  a4,-40(s0)
  3e:   97ba                    add a5,a5,a4
  40:   0007c783                lbu a5,0(a5)
  44:   f7f9                    bnez    a5,12 <.L3>

0000000000000046 <.LBE2>:
/Users/cyao/test/main.c:7
}
  46:   0001                    nop
  48:   0001                    nop
  4a:   70a2                    ld  ra,40(sp)
  4c:   7402                    ld  s0,32(sp)
  4e:   6145                    addi    sp,sp,48
  50:   8082                    ret

0000000000000052 <main>:
main():
/Users/cyao/test/main.c:9

int main() {
  52:   1141                    addi    sp,sp,-16
  54:   e406                    sd  ra,8(sp)
  56:   e022                    sd  s0,0(sp)
  58:   0800                    addi    s0,sp,16
/Users/cyao/test/main.c:10
    printf("Hello world");
  5a:   000007b7                lui a5,0x0
  5e:   00078513                mv  a0,a5
  62:   00000097                auipc   ra,0x0
  66:   000080e7                jalr    ra # 62 <main+0x10>
/Users/cyao/test/main.c:11
    example_inline("Hello! I am inlined");
  6a:   000007b7                lui a5,0x0
  6e:   00078513                mv  a0,a5
  72:   00000097                auipc   ra,0x0
  76:   000080e7                jalr    ra # 72 <main+0x20>
/Users/cyao/test/main.c:13

    return 0;
  7a:   4781                    li  a5,0
/Users/cyao/test/main.c:14
}
  7c:   853e                    mv  a0,a5
  7e:   60a2                    ld  ra,8(sp)
  80:   6402                    ld  s0,0(sp)
  82:   0141                    addi    sp,sp,16
  84:   8082                    ret

PS. 拆解后的代码中有颜色标记。


1
“--visualize-jumps=color”也是一种选项,可以为不同的箭头使用不同的颜色。当分支变得更加密集时,这可能非常有用,不像您的简单示例。例如,我在比终端+objdump更好的阅读汇编代码的方法?上的答案显示了带有FP比较+分支的一些代码的示例输出(实际上,如果没有颜色,由于无序下一个短分支旁边的另一个分支,它实际上有点难以阅读)。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接