1.相关疑问
1. 为什么在代码里使用了一个未定义过的函数(如add()),在编译阶段不会报错,在链接阶段会报错呢?
答:先说几个代码编译的结论:
单个\.c源文件文件被编译成机器码文件时,源文件中的所有变量名以及数组名都会变成地址偏移量;
类型信息都会变成指令的长度(int → subl, 地址 → subg);
循环会变成goto的方式实现;
函数的调用变成了使pc指针移动到被调方函数的地址;
由以上第四条可知,函数的调用依赖于被调函数的地址,编译后的文件可以使用nm xxx.o
来查看所有函数的地址和函数名的对应情况,在链接之前可以看到此时有的函数名还没有对应的地址,而将这些函数名和地址关联起来的操作就是链接阶段要做的事,在链接阶段无法为主调方找到被调方函数地址时就会报链接错误。
2. gcc编译器
1. 预处理指令
-
#include:引入头文件
-
#define:定义宏常量和宏函数
-
#if:根据指定条件判断是否编译后续代码块(
可作为测试代码开关
)// 使用示例: #define DEBUG 1 #if DEBUG // Debugging code printf("Debug mode is active.\n"); #endif
-
#ifdef:判断某个标识符是否已经定义(
可作为测试代码开关
)#ifdef ON //表示如果定义了ON,或命令行编译时用-D传了ON,就执行下面的输出 printf("ON is defined!\n"); #else printf("ON is not defined!\n"); #endif printf("main exit\n");
可以在命令行定义预处理器宏
:gcc -o test test.c -DON
-
#ifndef:判断某个标识符是否没有已经定义(
可用于头文件保护
)#ifndef TEST_H #define TEST_H #endif
-
#endif:结束条件编译的代码块
2. gcc常用指令
补充
列出所有函数:
nm test.o
查看可执行程序链接了哪些库:
ldd test
-
预处理:
gcc -E test.c -o test.i
-
编译:
gcc -S test.c -o test.s
-
汇编:
// 方式一:从汇编文件到机器码文件 as test.s -o test.o // 方式二:从预处理文件到机器码文件 gcc-13 -C test.i -o test.o // 方式三:从源文件到机器码文件 gcc-13 -C test.c -o test.o
-
链接:gcc -o test test.c 输出可执行文件
// 方式一:编译+链接(从源文件到可执行文件) gcc -o test test.c // 方式二:链接 gcc -o test test.o // 方式三:指定链接路径 gcc test1.o -o test1 -ladd // 还可以使用ld命令可以调用静态链接器 ld test.o [其他系统库文件] -o test
-
定义宏:
gcc test1.o -o test1 -D DEBUG
-
制定优化级别:
gcc test1.o -o test1 -O0 // 推荐O1级别
-
为gdb补充符号信息:
gcc test1.o -o test1 -O0 -g // 要用gdb调试时最好指定不优化,避免机器码和代码不一致影响调试
-
提示警告:
gcc test1.o -o test1 -Wall // 注意Wall是大写W
-
指定头文件搜路径:
gcc test1.o -o test1 -I "../h" //默认从当前路径下搜索
3. gdb调试
补充:
用gdb调试时,传递命令行参数的两种方式:
- 方式一:进入gdb后:用
set args xxx
- 方式二:进入gdb前:使用
gdb --args test xxx
启动
1. 常用指令
-
用-g生成可执行文件:
gcc –o test test.c -O0 –g
-
进入调试器:
gdb test
-
显示代码:
- 默认显示:
l/list //默认显示10行
- 显示指定信息:
l/list [文件名:]行号or函数名 //[]表示可有可无
- 默认显示:
-
运行:
r/run
-
退出:
q/quit
-
断点相关:
- 设置断点:
b/break 4 //在第四行设置断点
- 列出断点:
i b/info break // 查看断点号
- 删除断点:
delete 断点号
- 停止断点:
disable 断点号
- 激活断点:
enable 断点号
- 设置断点:
-
跳转:
- 下一步不进入函数:
n/next //F10
- 下一步进入函数:
s/step //F11
- 到下一个断点:
c/continue
- 跳出当前函数:
finsh
- 下一步不进入函数:
-
查看信息:
- 打印变量:
p/print
- 自动显示的表达式内容:
display 表达式
(自动显示内存:
display /1xw &i``) - 停止显示某个表达式:
undisplay 表达式
或者undisplay 编号(通过info display查看编号)
- 查看调用堆栈:
b t/back trace
- 查看内存:
x<n/f/u> //n表示内存的长度 f表示内存的格式 u表示内存的单位
- 清空信息:
Ctrl + L 组合键
,或者输入shell clear
命令(shell命令用于调用系统断点shell
)
- 打印变量:
2. 调试core文件
- 启用 core 文件自动生成
- 查看core文件是否存在限制:
ulimit -a
; - 关闭限制:
ulimit -c unlimited
;
- 查看core文件是否存在限制:
- 运行程序并生成 core 文件
- 如果未生成,则使用
man core
查看帮助文档解决;
- 如果未生成,则使用
- 使用 GDB 调试 core 文件:
gdb test core
; - 查看调试信息
- 查看调用栈:
bt
; - 查看栈帧信息:
frame 0
; - 显示寄存器的值:
info registers
; - 分析内存:
x
;
- 查看调用栈:
4. 静态库和动态库
库文件其实就是别人造好的“轮子”,别人写好并编译好后给用户使用的二进制代码。
静态库文件在程序的链接阶段被需要,而动态库文件在程序运行过程中也被需要。
1. 静态库
在程序链接阶段,库文件会被打包进最终的可执行程序。
gcc编译时,如果存在同名的动态(.so)和静态(.a)库文件,默认是使用动态文件,此时要使用静态库文件就必须要使用
-static参数指定优先静态链接,如果找不对应的\.a文件会链接失败。
特点:省心,但体积大且更新不方便。
打包命令:
// 1. 得到编译后的机器码文件
gcc -c add.c -o add.o
// 2. 生成静态库文件(库文件必须以lib开头)
ar crsv libadd.a add.o
// 3. 将静态库文件移动到"/usr/lib"或"/usr/local/lib"下,推荐前者
sudo mv libadd.a /usr/lib
// 4. 使用即可
gcc main.o -o main -ladd
2. 动态库
链接阶段不打包进程序,在程序运行时加载。
特点:体积小、更新方便(动态更新),但容易存在依赖问题。
打包命令:
// 1. 生成位置无关的目标代码,使用-fpid(Position Independent Code)
gcc -c add.c -o add.o -fpic
// 2. 生成动态库文件(库文件必须以lib开头)
gcc -shared add.o -o libadd.so
// 3. 将静态库文件移动到"/usr/lib"或"/usr/local/lib"下,推荐前者
sudo mv libadd.so /usr/lib
// 4. 使用即可
gcc main.o -o main -ladd
3. 产品动态更新
原理:利用版本号和软连接实现
- 第一步:生成带版本好的.so动态库文件,如:libadd.so.0.0.0、libadd.so.0.1.0
- 第二步:建立最新版本的软连接,
sudo ln -s libadd.so.0.1.0 libadd.so