C语言编译和调试
编译命令
使用gcc命令直接编译,文件很多时写起来很麻烦,而且容易出错,如
gcc -o asample.c bsample.c ...... xxx.out
于是就出现了make+makefile的编译工具,通过makefile描述如何编译、依赖关系、链接关系等,之后通过make命令进行编译
Makefile相当于将gcc等编译命令脚本化,Makkefile编写起来还是会比较麻烦,加上不具备跨平台等特性,所以出现cmake、meson等更加高级的工具用于生成构建脚本
通过cmake、meson等工具编译工程时,简化了编译脚本同时也可以将每个文件的编译命令等输出成编译数据库,编译数据库是一个json格式,它提供了编译源文件时的命令、参数、源文件、工作目录等信息,可以基于编译数据库对C/C++代码进行分析等。一般的生成文件名为compile_commands.json。
编译过程
-
预处理,展开头文件、宏替换、去掉注释,使用-E参数表示只进行预处理,-o输出处理后的C语言代码
-
编译,将C语言转成汇编语言,使用-S参数表示只是处理到编译这个步骤,-o输出处理后的汇编代码
-
汇编,将汇编转成二进制文件,使用-C参数表示只处理到汇编这个步骤,-o输出处理后的二进制文件,格式ELF(Executable and Linkable Format)
-
链接,将依赖的其他文件整合进来,包括静态链接和动态链接两种
静态链接,将所引用的其他文件里面的信息全部加载进来一起打包,相当于直接把代码放进来一起编译打包
动态链接,先记录引用的库名称、位置等信息,真正执行时由链接器去加载动态库将真实引用引进。
依赖库又分为动态库和静态库,编译时通过-shared参数可以生成动态库,-static生成静态库。在链接时,如果链接的目标库是静态库则执行静态链接,如果是动态库则执行动态链接,如果两个都有,优先动态,除非有静态参数-static会优先选择静态链接库。
可以通过-L参数指定库的位置,链接时会去该目录寻找对应库文件。也可以通过-T参数指定链接脚本定制化链接行为,可以定制化控制输出段(Segment)的布局,包括代码段、数据段等,还可以控制符号的定位和重定位信息的存储等
ps:
通常出于安全考虑会使用-pie编译参数生成随机代码段的基地址,一般和ASLR配合使用,但当指定-pie参数时不能在脚本定义基地址否则将不生效,而默认的链接脚本中定义代码段的基地址为0x400000(优化的考虑),导致-pie参数不会生效。
ELF文件格式
官方文档https://refspecs.linuxfoundation.org/elf/elf.pdf
ELF(Executable and Linkable Format)是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件的文件格式。是Linux的主要可执行文件格式。对于ELF文件主要由如下部分组成
文件头,保存版本信息、大小、文件类型(可执行、静/动态库等)、程序入口地址(可执行文件来说其他类型都是0)、program header偏移、section header偏移等
program header,只有可执行文件类型有,保存segments表,segments是由多个section组成的,加载时是以segments为单位进行加载的,更加方便权限管理等,还包含
section header,保存section表,对文件中所有的section进行描述,包括名字、类型(常见的符号表、数据段、代码段、BSS、GOT表等是其中的一种或者多种类型保存的,不同的类型有不同布局)、起始地址、大小等信息,相当所有sections的目录
Linux上可以通过readelf 和 objdump等工具读取文件
进程空间
linux通过虚拟内存的方式实现多进程内存隔离,每个进程都有一个页表用于映射虚拟地址和物理地址,进程的页表由内核维护。
为了区分和隔离用户进程和内核进程,用户进程地址空间中高位地址并不能使用,而是分配给内核使用,比如在32位系统中,1-3G用户态进程使用,3-4G为内核态进程使用。这样可以多个用户进程共享一个内核进程。
Linux中每个进程地址空间通过区域划分进行管理,每个区域有单独的访问权限、ops(缺页处理函数等),如下低位为代码区即进程执行文件加载区域(ELF可能包含数据段、代码段、BSS等)之后是堆区(内存的动态申请等),之后为共享区(链接文件、映射文件等),之后为栈区,最高位为内核空间。
常见操作
1、mmap文件到共享区时,当访问对应文件映射的地址时,检查对应的区是否有操作权限,可以读,但是没有地址空间映射,没有对应的物理页,内核态发生缺页中断执行该区域的ops缺页处理,去内核态寻找是否有对应的高速缓存页……最后内核空间的缓存页进行映射,此时用户进程中的虚拟地址和内核态缓存页地址指向同一块物理地址
2、申请内存,即mmap映射一个匿名文件,访问时检查权限,可以读,但是没有地址空间映射,没有对应的物理页,内核态发生缺页中断执行该区域的ops缺页处理,触发内核态内存分配和地址空间映射保存到页表中
GDB调试
通过原理实现如下
-
通过-g参数编译程序,执行文件会包含增加调试信息,包括保留变量名、去掉优化等方便进行断点等
-
通过父子进程实现信息的读取打印,GDB为父进程,目标进程为子进程,父可以读取子进程堆栈、寄存器等
-
通过ptrace控制子进程执行,通过插入代码实现断点插入等
GDB使用如下
安装
apt install gdb
编译时增加-g参数
gcc -g main.c
#DPDK中meson编译增加-Dc_args="-g -O0"
启动
gdb ./a.out
GDB常用命令如下
run 运行程序到断点除停止
continue 继续执行,到下一个断点停止。
step 单步调试,如果遇到函数则进入函数。
next 执行下一条指令
break n 在第n行设置断点
break func 在函数func入口处设置断点
info break 显示当前程序的断点设置情况
list 列出程序的源代码,默认每次显示10行
list 行号 显示当前文件以行号为中心的前后10行代码
list 函数名 显示函数源码
print 变量 打印变量的值