编译和调试C/C++程序常用到的一些知识点

做个笔记,记录下编译和调试C/C++程序常用的一些知识点,主要就是gcc、gdb的一些常用使用方法。

gcc

如图所示,先看下C/C++代码生成可执行文件的过程,共4步:预处理、编译、汇编、链接。

一步到位的编译命令为:

1
gcc -Wall -g test.c -o test

预处理

命令为:

1
gcc -E test.c -o test.i 或 gcc -E test.c

生成的test.i文件,可以通过vim或emacs直接打开。

gcc的-E选项,可以让编译器在预处理后停止,并输出预处理结果。比如预处理结果就是将stdio.h 文件中的内容插入到test.c中了。

编译为汇编代码(Compilation)

预处理之后,可直接对生成的test.i文件编译,生成汇编代码:

1
gcc -S test.i -o test.s

gcc的-S选项,表示在程序编译期间,在生成汇编代码后,停止,-o输出汇编代码文件。

汇编(Assembly)

对于上一小节中生成的汇编代码文件test.s,gas汇编器负责将其编译为目标文件,如下:

1
gcc -c test.s -o test.o

连接(Linking)

gcc连接器是gas提供的,负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件。附加的目标文件包括静态连接库和动态连接库。

对于上一小节中生成的test.o,将其与C标准输入输出库进行连接,最终生成程序test

1
gcc test.o -o test

一些技巧

生成的汇编代码可以使用vim或emacs直接打开,如下代码文件名为main.c。
x86系统:

1
gcc -S -O2 code.cpp

x64系统:

1
gcc -m32 -S -O2 code.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
void f1(int *a)
{
for (int i = 0; i < 3;i++) {
a[i] = a[i] + 2;
}
}
void f2(int *a)
{
a[0] = a[0] + 2;
a[1] = a[1] + 2;
a[2] = a[2] + 2;
}
int main() {
int a[3] = {0,1,2};
f1(a);
f2(a);
return 0;
}

注意使用-O2编译,生成的.s文件可以直接打开,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.LFB961:
.cfi_startproc
.cfi_personality 0x0,__gxx_personality_v0
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
movl 8(%ebp), %eax
addl $2, (%eax)
addl $2, 4(%eax)
addl $2, 8(%eax)
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE961:
.size _Z2f1Pi, .-_Z2f1Pi
.p2align 4,,15

objdump

最常用命令为:

1
objdump -S code

即同时显示源代码和汇编代码,部分代码为:

1
2
3
4
5
6
7
8
9
10
11
void f1(int *a)
{
for (int i = 0; i < 3;i++) {
a[i] = a[i] + 2;
400680: 83 07 02 addl $0x2,(%rdi)
400683: 83 47 04 02 addl $0x2,0x4(%rdi) // 取出寄存器的值,然后加上4,得到的值作为地址,间接寻址得到需要的数据
400687: 83 47 08 02 addl $0x2,0x8(%rdi)
}
}
40068b: c3 retq
40068c: 0f 1f 40 00 nopl 0x0(%rax)

二进制文件也可以使用vim或emacs打开,使用十六进制打开即可:

1
vim命令为:%!xxd

emacs命令为:

1
ALT+X hexl-mode

当然也可以使用hexdump查询十六进制(od也可以,只是默认八进制),命令为:

1
hexdump -C 二进制文件 | more

gdb

gdb有2种使用方式,一个是直接挂载指定的进程,命令为

1
gdb -p <pid>

另一种方式就加载二进制文件或core文件,命令为:

1
gdb -c core 得到二进制文件地址

加载二进制文件方式:

1
2
3
gdb binary
gdb binary core

运行相关

1
2
3
4
5
6
7
8
9
10
11
run:简记为 r ,其作用是运行程序,当遇到断点后,程序会在断点处停止运行,等待用户输入下一步的命令。
continue (简写c ):继续执行,到下一个断点处(或运行结束)
next:(简写 n),单步跟踪程序,当遇到函数调用时,也不进入此函数体;此命令同 step 的主要区别是,step 遇到用户自定义的函数,将步进到函数中去运行,而 next 则直接调用函数,不会进入到函数体内。
step (简写s):单步调试如果有函数调用,则进入函数;与命令n不同,n是不进入调用的函数的
until:当你厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体。
until+行号: 运行至某行,不仅仅用来跳出循环
finish: 运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。
call 函数(参数):调用程序中可见的函数,并传递“参数”,如:call gdb_test(55)
frame/f 帧编号: 选择栈帧
start:开始执行程序,停在main函数第一行语句前面等待命令
quit:简记为 q ,退出gdb

break

1
2
3
4
5
6
7
8
b main // main函数设置断点
b 8 // 第8行设置断点
b main.cpp:main //main文件的main函数设置断点
b main.cpp:8 //main文件的第8行设置断点
delete 断点号n //删除第n个断点
disable 断点号n //暂停第n个断点
enable 断点号n //开启第n个断点
delete breakpoints //清除所有断点

info

1
2
3
4
5
6
7
查看当前程序栈的信息: info frame----list general info about the frame
查看当前程序栈的参数: info args---lists arguments to the function
查看当前程序栈的局部变量: info locals---list variables stored in the frame
查看当前寄存器的值:info registers(不包括浮点寄存器) info all-registers(包括浮点寄存器)
查看当前栈帧中的异常处理器:info catch(exception handlers)
查看断点信息: info break
查看当前线程: info threads

查看源码

1
2
3
list :简记为 l ,其作用就是列出程序的源代码,默认每次显示10行。
list 行号:将显示当前文件以“行号”为中心的前后10行代码,如:list 12
list 函数名:将显示“函数名”所在函数的源代码,如:list main

打印

1
2
3
p/x var //以十六进制打印整数var
p/a addr //打印十六进制形式的地址
p/u var //变量var无符号整数打印

分割窗口

1
2
3
4
5
6
layout:用于分割窗口,可以一边查看代码,一边测试
layout src:显示源代码窗口
layout asm:显示反汇编窗口
layout regs:显示源代码/反汇编和CPU寄存器窗口
layout split:显示源代码和反汇编窗口 //推荐这个命令,汇编的si/ni对应源码的s/n,si会进入汇编和C函数内部,ni不会
Ctrl + L:刷新窗口

多线程

1
2
3
info threads //查看当前进程的线程
thread <id> //切换线程
f 3 //选择栈帧

参考链接:
http://www.cnblogs.com/ggjucheng/archive/2011/12/14/2287738.html
http://man.linuxde.net/objdump
http://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/gdb.html