Skip to content

Latest commit

 

History

History
250 lines (172 loc) · 10.5 KB

编译.md

File metadata and controls

250 lines (172 loc) · 10.5 KB

预编译(预处理)(Prepressing)

  • 指令gcc -E hello.c -o hello.i

主要处理#开始的预编译指令

  • 宏定义
  • 内联函数(inline function)
  • #、##运算符和可变参数
  • 条件预处理指示 : #ifdef ,#if 0等
  • 其它预处理特性:C标准规定了几个特殊的宏,在不同的地方使用可以自动展开成不同的值,常用的有__FILE__和__LINE__(是编译器内建的特殊的宏)

#、##运算符和可变参数

在函数式宏定义中,#运算符用于创建字符串,#运算符后面应该跟一个形参

#define STR(S) #S
printf("%s\n", STR(Hello World));
// 打印结果:Hello World

在宏定义中可以用##运算符把前后两个预处理Token连接成一个预处理Token,和#运算符不同,##运算符不仅限于函数式宏定义,变量式宏定义也可以用

#define FUNC1(A,B) A##B
void FUNC1(Hello, World)(void)
{
}
void FUNC1(,World)(void)
{
}
可定义函数HelloWorld();
World();

在宏定义中,可变参数的部分用__VA_ARGS__表示,实参中对应...的几个参数可以看成一个参数替换到宏定义中__VA_ARGS__所在的地方。

#define my_print1(...)  printf(__VA_ARGS__)   //my_print1("i=%d,j=%d\n",i,j)  //正确打印
#define my_print2(fmt,...)  printf(fmt,__VA_ARGS__)  //my_print2("i=%d,j=%d\n",i,j) //正确打印
//my_print2("iiiiiii\n")       //编译失败打印,因为扩展出来只有一个参数,至少要两个及以上参数
#define my_print3(fmt,...)  printf(fmt,##__VA_ARGS__)  //不管几个参数都可以打印

编译(Compilation)

编译过程整一般可以分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。

  • 指令gcc -S hello.c -o hello.s

汇编(Assembly)

指令: gcc -c hello.s -o hello.o, as hello.s -o hello.o

链接(Linking)

指令: ld -static xx.o

链接过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等这些步骤

编译选项

gcc 编译 选项 汇总 - 知乎 (zhihu.com)

  • -Wundef :当 #if指令中用到未定义的宏时给出警告`
  • -Wno-strict-aliasing :当代码可能破坏强重叠规则时不给出警告(相反的是-Wstrict-aliasing)
  • -Wno-format-security:与-Wformat-security 当使用格式字符串的函数可能导致安全问题时给出警告。相反。
  • -fdata-sections: 使compiler为每个data item分配独立的section
  • -ffunction-sections: 使compiler为每个function分配独立的section

GCC链接操作是以section作为最小的处理单元,只要一个section中的某个符号被引用,该section就会被加入到可执行程序中去。因此,GCC在编译时可以使用 -ffunction-sections和 -fdata-sections 将每个函数或符号创建为一个sections,其中每个sections名与function或data名保持一致。而在链接阶段, -Wl,–gc-sections 指示链接器去掉不用的section(其中-wl, 表示后面的参数 -gc-sections 传递给链接器),这样就能减少最终的可执行程序的大小了。

  • -fno-omit-frame-pointer :不要去掉函数调用时的frame pointer,相反的是-fomit-frame-pointer
  • -fno-stack-protector :禁用栈保护措施,相反的是-fstack-protector
  • -fno-var-tracking:默认选项为-fvar-tracking,会导致运行非常慢
  • -fstack-protector-all :栈保护选项,平时不开启,用于调试定位栈越界问题
  • -Wl,--version-script x.map : 可用于不想暴露太多无关接口,减小so的大小的时候

符号表定位

编译选项中加入 -Wl,-Map=mapfile,-trace,-trace-symbol=func_to_trace (-trace-symbol用于追踪指定符号表,即那个库引入该函数)

march

-march=cpu-type
优化选项。指定目标架构的名字,以及(可选的)一个或多个功能修饰符。 此选项的格式为:
-march = arch {+ [no] feature} * 碰到下面这个错误,就可能需要指定cpu-type了 swp{b} use is obsoleted for ARMv8 and later CFLAGS增加-march=armv7-a 选项

ASAN编译时会使用

ASAN_CFLAGS += -fno-stack-protector -fno-omit-frame-pointer -fno-var-tracking -g1

严格规范代码的选项

将-Werror=改为-W则为告警,不会报错

  • -Werror=implicit-function-declaration : 对隐式函数声明
  • -Werror=undef : 当 #if指令中用到未定义的宏时
  • -Werror=unused-but-set-variable : 变量定义未使用
  • -Werror=unused-variable : 有未使用的变量
  • -Werror=discarded-qualifiers : 缺少限定符,如加了const限定符的成员函数中,不能够调用 非const成员函数
  • -Werror=incompatible-pointer-types : 从不兼容的指针类型赋值
  • -Werror=parentheses : 可能缺少括号的情况
  • -Werror=unused-function : 未使用的函数
  • -Werror=misleading-indentation : 不正确的缩进
  • -Werror=pointer-sign : 赋值时如指针符号不一致
  • -Werror=endif-labels : #endif指令末尾有额外的标记
  • -Werror=int-conversion : 强制转换给出报错
  • -Werror=unused-const-variable : 未使用踩到const变量
  • -Werror=unused-label : 有未使用的标号
  • -Werror=comment : 对可能嵌套的注释和长度超过一个物理行长的C++ 注释
  • -Werror=char-subscripts : 当下标类型为“char”时
  • -Werror=return-type : 当 C函数的返回值默认为“int”,或者 C++函数的返回类型不一致
  • -Werror=format : printf/scanf/strftime/strfmon中的格式字符串异常
  • -Werror=pointer-to-int-cast : 将一个指针转换为大小不同的整数

目标文件

file命令可以看文件格式

file *.o, file /bin/bash 等

ELF文件类型

可重定位文件Relocatable File

  • 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类。

  • 如Linux的.o 文件,Windows的obj文件

可执行文件Executable File

  • 这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名。

  • 如bin/bash文件Windows的exe

共享目标文件Shared Object File

  • 这种文件包含了代码和数据,可以在以下两种情况下使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的部分来运行

  • 如Linux的.so,如/ib glibc-2.5.so windows的DLL

核心转储文件Core Dump File

  • 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件

  • 如linux下的coredump

符号

函数签名

  • 函数签名包含了一个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间及其他信息。函数签名用于识别不同的函数。

符号修饰

  • 在编译器及连接器处理符号时,他们使用某种名称修饰的方法,使得每个函数签名对应一个修饰后的名称(Decorated Name)

  • GCC的基本C++名称修饰方法:所有的符号都以_Z开头,对于嵌套的名字(在名称控件或在类里面的),后面紧跟N,然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以E结尾。比如 N::C::func,修饰后就是_ZN1N1C4funcE。对弈一个函数来说,它的参数列表紧跟在E后面,对于int类型来说,就是字母i,所以N::C::func(int) 函数签名经过修饰为_ZN1N1C4funcEi。 binutils 里面提供了一个叫 c++filt 工具可以来解析被修饰过的名称

  • 全局变量和静态变量也会进行签名和名称修饰

extern "C"

  • C写的函数头文件最好加上,方便兼容C和C++引用

弱符号和强符号

  • 强符号(Strong Symbol)

  • 弱符号(Weakl Symbol)

  • C/C++,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。可以通过 attribute((weak)) 来定义任何一个强符号或弱符号

弱引用和强引用

  • 强引用:找不到符号的定义,就会报符号未定义错误; 弱引用:该符号有定义,则链接器将该符号的引用决议;否则,链接器对于该引用不报错。编译不报错,运行会报错。
__attribute__ ((weakref)) void foo();
int main()
{
    foo();
}

静态链接

空间与地址分配

  1. 按序叠加

  2. 相似段合并

  • 现代编译器采用的都是此方案

  • 两步链接:1) 空间与地址分配,2) 符号解析与重定位

  • objdump -h x.o 查看链接前后地址的分配情况

重定位表(Relocation Table)

  • 保存重定位相关的信息

  • .text --> .rel.text

  • .data --> .rel.data

符号解析

  • 每个重定位的入口都是对一个符号的引用,那么当链接器需要对某个符号的引用进行重定位时,它就要确定这个符号的目的地址,这时候链接器就会去查找由所有输入目标文件的符号表组成的全局符号表,找到相应符号后进行重定位 readelf -s a.o 可以查看GLOBAL, UND,LOCAL等类型

动静态库编译

静态库

  1. 编译成.o gcc *.c -c -I..
  • -I 指定头文件的路径
  • .. 可以用具体路径代替 如 -I/home/neojan
  • -c 生成 .o 结尾的文件
  1. 将.o打包成.a ar rs libcalculator.a *.o
  • *.o 表示文件中所有以 .o 结尾的文件
  • calculator.a为静态库名称
  • 此时会生成一个libcalculator.a 的文件

动态库

编译打包

  1. 编译成.o gcc *.c -c -fPIC -I.. fPIC : Position Independent Code, 用于生成位置无关代码

  2. 将.o打包成共享库.so gcc -shared -o libmymath.so *.o

  3. 以上可以用一句gcc test.c -fPIC -shared -o libtest.so

  4. 动态链接库:gcc gjobread.c -Wl,-rpath, ../xml2_arm/lib/libxml2.so.2

  5. 如果静态库和动态库存放在同一文件下,优先匹配动态库,动态库体积更小。

  6. gcc main.c -Lsofun -lmymath 这里mymath前不需要加lib

如何避免库函数暴露?

提供动态库时,建议只让动态库暴露需要的函数。且函数不要与系统函数重名。
gcc编译器直接提供了`-fvisibility=hidden`编译选项,直接让库函数global可见变成local可见,可以使用`__attribute__((visibility("default")))`将需要的函数暴漏在外面。
假如确实需要使用不同动态库中的同名函数,建议使用dlopen, dlsym, dlclose进行动态载入。

ldd

  • ldd 文件名:查看可执行文件链接了哪些 系统动态链接库