1. C语言的编译与执行
C语言编译的流程:
源程序->预处理器->修改后的源程序(*.i)->编译器->目标汇编程序->汇编器->可重定位的机器代码(加上 库文件和可重定位的目标文件)->链接器->目标机器代码
1) 预处理阶段
简而言之就是文本替换,比如 include <stdio.h>
就会把 stdio.h 文件中的内容插入到源文件中
主要有以下几方面的工作:
宏定义
#define PI 3.1415926
条件编译
#ifdef、#ifndef、#else、#elif、#endif
头文件
#include
- 如果头文件是系统头文件,用
#include <>
- 如果头文件是自定义头文件,用
#include ""
2) 编译阶段
编译阶段主要完成对经过预处理输出的 .i 文件进行编译和优化。编译器首先对代码进行词法分析和语法分析,在确认所有指令都符合语法规则之后,将其翻译成等价的中间代码或汇编代码,然后执行优化处理
3) 汇编阶段
汇编器生成的目标文件,通常由三个部分组成
- 代码段:存放程序的指令,通常不允许写入
- 数据段:存放已初始化的全部变量和局部静态变量
- BSS(Block Started by Symbol)段:存放未初始化的全局变量和局部静态变量,与上一段分开是因为,此时变量的值为0,故不需要在目标文件中为其分配空间,只要记录需要分配空间的大小,供程序加载时使用即可
Linux中目标文件使用ELF(Executable and Linkable Format)格式,主要由以下三种:
- 可重定位的目标文件:即常说的目标文件或者静态库文件,Linux下后缀位.a和.o的文件
- 共享目标文件:即共享库文件,也就是动态链接库文件,Linux下后缀位.so的文件
- 可执行目标文件:即一个可以被操作系统给用来创建一个进程的文件,可直接运行
4) 链接阶段
可以分为静态链接和动态链接两种
静态链接
被引用函数的代码将从包含这些代码的静态链接库中被拷贝到可执行程序中
动态链接
链接程序此时所做的知识在最终的可执行程序中记录下共享对象的名字以及其他少量的登记信息
总结
使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份共享对象的代码。但并不是使用动态链接就一定比使用静态链接优越,在某些情况下,动态链接可能带来一些性能上的损害。
2. GCC及主要运行参数介绍
GCC时GNU组织编译器的集合(GNU Compiler Collection),能够编译用C、C++、Object C、Fortran、Ada和Go语言编写的程序。通常我们所说的gcc,实际上只是GCC中的一个专门针对C和C++语言的编译器
gcc常用选项列表
后缀名 | 所对应的语言 |
-E | 只进行预编译 |
-S | 只编译不汇编 |
-c | 只编译不链接,生成目标文件 “.o” |
-o file | 把输出的文件输出到file里 |
-I dir | 在头文件的搜索路径列表中添加 “dir” 目录 |
-L dir | 在库文件的搜索路径列表中添加 “dir” 目录 |
e.g.以 “Hello World” 程序为例
#include <stdio.h>
int main(int argc, char **argv){
printf("Hello World!\n");
return 0;
}
预处理
生成 “.i” 的修改后的源程序文件,注意这里的 “-E” 要大写
$gcc -E hello.c -o hello.i
编译
生成 “.s” 的目标汇编程序文件,注意这里的 “-S” 要大写
$gcc -S hello.i -o hello.s
汇编
生成 “.o” 的目标汇编程序文件,此处缺省 “-o” 命令也会输出以源文件名称的 “.o” 文件
$gcc -c hello.s -o hello.o
以上三步也可以直接一句命令
$gcc -c hello.c
若要查看 “.o” 文件,用以下命令:(因为Linux下,二进制文件默认使用 ELF 格式存储,无法用文本编辑其直接查看其内容)
$readelf -a hello.o
链接
$gcc hello.o -o hello
输入命令 ./hello
执行文件
如果不指定输出文件名,默认为 “a.out”
以上四步也可以直接一句命令
$gcc hello.c -o hello
其他参数
-O和-O2: “-O” 选项是让gcc对源代码进行基本优化; “-O2” 选项是让gcc产生尽可能小和尽可能快的代码,使用 “-O2” 选项将使编译的速度比使用 “-O” 时慢,但通常产生的代码执行速度会更快。此时,建议在使用了 “-O2” 选项后要验证程序的正确性
3. Makefile文件语法及示例
简而言之,就是完成一个大工程项目中的链接调用关系的批处理脚本文件
生成Makefile文件(不需要后缀名):
$vi Makefile
Makefile文件的主体由一些列规则(rules)组成,每条规则的形式如下:
<target>:<prerequisites>
[Tab]<commands>
[Tab]<commands>
...
一条规则由一个目标(target)、若干个前置条件(prerequisites)和一系列命令(commands)组成,目标是必须的,其他均可选,命令所在的行必须由制表符(Tab键)开始
每条规则就明确两件事:构建目标的前置条件是什么以及如何构建
以 “hello.c” 来举例
hello : hello.o
gcc hello.o -o hello
hello.o : hello.c
gcc -c hello.c
clean :
rm hello hello.o
用 make hello
进行make
用 make clean
删除编译产生的中间文件
如果直接 make
,则只会执行第一个目标,即 make hello
4. 调试及gdb的使用
gdb是一个由GNU开源组织发布的、Unix/Linux下的、基于命令行的程序调试工具(不怎么会用,慢慢用吧)
1) 启动gdb
对C/C++程序的调试,只要在编译源文件时加上 “-g” 选项
$gcc -g hello.c -o hello
运行以下命令调试可执行文件
$gdb <program>
其中<program>是需要调试的可执行文件名,如
$gdb hello
如果在程序正常编译执行后发现错误,系统会转存(dump)相关数据到一个core文件中,此时,可以通过以下命令调试core文件
$gdb <program><core dump file>
假如hello程序的dump文件是core.11127,则
$gdb hello core/11127
如果程序是一个服务程序,那么可以通过这个服务程序运行时的PID对服务程序进行调试,如
$gdb <program><PID>
如:
$gdb hello 11127
此时,gdb会自动attach上11127的服务程序并对其进行调试
2) gdb交互命令
启动gdb后,进入到交互模式,通过以下命令完成对程序的调试
1. 运行
run(简写r) | 运行程序,遇到断点会在断点处停止运行 |
continue(简写c) | 继续执行到下一断点或直到程序运行结束 |
next(简写n) | 单步跟踪,一行一行执行,不会进入子函数 |
step(简写s) | 单步调试,如果有函数进入函数 |
until | 使程序一直运行到退出循环体 |
until n | 运行至程序指定的第n行 |
finish | 运行程序直到当前函数完成返回,并打印函数返回时的堆栈地址、返回值及参数值等信息 |
call fun(args) | 调用程序中可见函数fun,并传递参数args给该函数。例如,call set_value(32)调用函数set_value,并给该函数传递32作为参数 |
quit(简写q) | 退出gdb |
2. 设置断点
break n(简写b n) | 在第n行处设置断点。此外,可在行号前带上代码路径和代码名称,如: b /home/foo/src/hello.c:578 |
break [break-args] if (condition) | 条件断点设置,如: b main if argc>1 表示当main函数的参数argc大于1时激活该断点 |
break func(简写b func) | 在函数func()的入口处设置断点,如: b cb_button |
delete n | 删除第n个断点,从上往下的顺序,从1开始计数 |
disable n | 使第n个断点失效 |
enable n | 开启第n个断点 |
clear n | 清除第n行上的断点 |
info breakpoints(简写info b) | 显示当前程序的断点设置情况 |
delete breakpoints | 清除所有断点 |
3. 查看源代码
list(简写l) | 列出程序的源代码,默认每次10行 |
list n | 显示当前文件以第n行为中心的前后10行代码 |
list func | 显示名为func的函数源代码 |
list | 若不带任何参数,则接着上一次list命令直接输出其下内容 |
4. 打印表达式
print 表达式(简写p) | 显示表达式的值,表达式可以使当前任何正在被测试的程序中的有效表达式,包括数字、变量、函数调用 |
print ++a | 把a中的值加1并显示出来 |
print name | 显示字符串name的值 |
watch 表达式 | 设置一个监视点,一旦被监视的表达式的值改变,gdb将强行终止正在被调试的程序 |
whatis | 查询变量或函数 |
info function | 查询函数 |
info locals | 显示当前堆栈页的所有变量 |
5. 分割窗口
layout src | 显示源代码窗口 |
layout split | 显示源代码和反汇编窗口 |
Ctrl + L | 刷新窗口 |
交互模式下直接回车的作用是重复上一指令,对于单步调试非常方便