背景
为什么使用ullib有时会出现 undefined reference error 的错误?
为什么在动态链接库里ul_log会把日志输出到屏幕上?
为什么用-static 编译有时候会报warning?
我们在使用基础库或者第三方库的时候,经常遇到这样那样的问题,本文结合公司目前的主要环境,说明库的原理,使用的注意事项。
从程序到可执行文件
从hello world 说起
#includeint main() { printf("hello world\n"); return 0; }
上面的程序如果要编译,很简单
gcc hello.c
然后./a.out就可以运行,但是在这个简单的命令后面隐藏了许多复杂的过程
一般来说,可以把这样的过程分成4个, 预编译, 编译, 汇编和链接
预编译
这个过程包括了下面的步骤
- 宏定义展开,所有的#define 在这个阶段都会被展开
- 预编译命令的处理,包括#if #ifdef 一类的命令
- 展开#include 的文件,像上面hello world 中的stdio.h , 把stdio.h中的所有代码合并到hello.c中
- 去掉注释
gcc -E hello.c -o hello.i
生 成的 hello.i 就是经过了预编译的结果 在预编译的过程中不会太多的检查与预编译无关的语法(#ifdef 之类的还是需要检查, #include文件路径需要检查), 但是对于一些诸如 ; 漏掉的语法错误,在这个阶段都是看不出来的。 写过makefile的人都知道, 我们需要加上-Ipath 一系列的参数来标示gcc对头文件的查找路径
小提示:
1.在一些程序中由于宏的原因导致编译错误,可以通过-E把宏展开再检查错误 , 这个在编写 PHP扩展, python扩展这些大量需要使用宏的地方对于查错误很有帮助。
2. 如果在头文件中,#include 的时候带上路径在这个阶段有时候是可以省不少事情, 比如 #include , 这样在gcc的-I参数只需要指定一个路径,不会由于不小心导致,文件名正好相同出现冲突的麻烦事情. 不过公司由于早期出现了lib2和lib2-64两个目录, 以及头文件输出在include 目录下, 静态发布等一些历史原因, 有些时候使用带完整路径名的方式不是那么合适( 比如 #include 中间有一个include 显的很别扭).
不过个人认为所有的#include 都应该是尽量采用从cvs 根路径下开始写完整路径名的方式进行预编译的过程,只是受限于公司原有习惯和历史问题而显的不合适, 当然带路径的方式要多写一些代码,也是麻烦的事情, 路径由外部指定相对也会灵活一些.
编译
这个过程才是进行语法分析和词法分析的地方, 他们将我们的C/C++代码翻译成为 汇编代码, 这也是一个编译器最复杂的地方
使用命令
gcc -S hello.i -o hello.s
可 以看到gcc编译出来的汇编代码, 现代gcc编译器一般是把预编译和编译合在一起,使用cc1 的程序来完成这个过程,在我们的开发机上有些时候一些同学编译大文件的时候可以用top命令看一个cc1的进程一直在占用时间,这个时候就是程序在执行编 译过程. 后面提到的编译过程都是指 cc1的处理包括了预编译与编译.
汇编
现在C/C++代码已经成为汇编代码了,直接使用汇编代码的编译器把汇编变成机器码(注意还不是可执行的) .
gcc -c hello.c -o hello.o
这里的hello.o就是最后的机器码, 如果作为一个静态库到这里可以所已经完成了,不需要后面的过程.
对于静态库, 比如ullib, COM提供的是libullib.a, 这里的.a文件其实是多个.o 通过ar命令打包起来的, 仅仅是为了方便使用,抛开.a 直接使用.o 也是一样的
小提示:
1. gcc 采用as 进行汇编的处理过程,as 由于接收的是gcc生成的标准汇编, 在语法检查上存在不少缺陷,如果是我们自己写的汇编代码给as去处理,经常会出现很多莫名奇妙的错误.
链接
链接的过程,本质上来说是一个把所有的机器码文件组合成一个可执行的文件 上面汇编的结果得到一个.o文件, 但是这个.o要生成二执行文件只靠它自己是不行的, 它还需要一堆辅助的机器码,帮它处理与系统底层打交道的事情.
gcc -o hello hello.o
这样就把一个.o文件链接成为了一个二进制可执行文件. 我们提供的各种库头文件在编译期使用,到了链接期就需要用-l, -L的方式来指定我们到底需要哪些库。 对于glibc中的strlen之类常用的东西编译器会帮助你去加上可以不需要手动指定。
这个地方也是本文讨论的重点, 在后面会有更详细的说明
小提示:
有些程序在编译的时候会出现 "linker input file unused because linking not done" 的提示(虽然gcc不认为是错误,这个提示还是会出现的), 这里就是把 编译和链接 使用的参数搞混了,比如
g++ -c test.cpp -I../../ullib/include -L../../ullib/lib/ -lullib
这样的写法就会导致上面的提示, 因为在编译的过程中是不需要链接的, 它们两个过程其实是独立的
静态链接
链接的过程
这里先介绍一下,链接器所做的工作
其实链接做的工作分两块: 符号解析和重定位
符号解析
符号包括了我们的程序中的被定义和引用的函数和变量信息
在命令行上使用 nm ./test
test 是用户的二进制程序,包括
可以把在二进制目标文件中符号表输出
00000000005009b8 A __bss_start00000000004004cc t call_gmon_start00000000005009b8 b completed.10000000000500788 d __CTOR_END__0000000000500780 d __CTOR_LIST__00000000005009a0 D __data_start00000000005009a0 W data_start0000000000400630 t __do_global_ctors_aux00000000004004f0 t __do_gl