1、编译隐藏的过程
假设有一个源文件:
#include
#pragma pack(2)
#define N 666
#define PR_D(x) printf(#x" = %d\n", x )
#define CONNECT(a,b) (a##b)
#ifdef N
#define VERSION "2.0"
#else
#define VERSION "0.1"
#endif
int ijk;
int main(void)
{
printf("(%s,%s,%s,%d)\n", __TIME__, __FILE__, __FUNCTION__, __LINE__); /* print __LINE__ */
printf("%s\n", VERSION); // print version
PR_D(ijk);
printf("%d\n", CONNECT(i,jk) );
return 0;
}
通常用 gcc
完整的编译命令是 gcc hello.c -o hello
上面的完整编译可以分解为 预处理、编译、汇编、链接。
预处理可以用命令 cpp hello.c > hello.i
或者 gcc -E hello.c -o hello.i
,
$ gcc -E hello.c
...
# 2 "hello.c" 2
#pragma pack(2)
# 14 "hello.c"
# 14 "hello.c"
int ijk;
int main(void)
{
printf("(%s,%s,%s,%d)\n", "07:03:49", "hello.c", __FUNCTION__, 18);
printf("%s\n", "2.0");
printf("ijk"" = %d\n", ijk );
printf("%d\n", (ijk) );
return 0;
}
可以看到:
#include
被展开了,头文件内容太多不放在这里了。
#define
被展开了,除了__FUNCTION__
,其他都替换了。
#if
和 #ifdef
等条件编译被展开了,VERSION 已经替换成 "2.0" 了。
注释已经被删除了。
#pragma
会保留,在后面编译阶段处理。
添加了行号、文件名等标识,以便下一个过程编译可以产生调试用的行号,以及如果编译报错时可以打印行号。
如果代码里面有太多条件编译,不知道哪个定义了,哪个没定义时,可以查看 gcc -E
的信息,或者直接使用#pragma message
:
#ifdef N
#pragma message("N defined!")
#else
#pragma message("N undeclared!")
#endif
编译阶段的命令是 gcc -S hello.i -o hello.s
汇编段的命令是 as hello.s -o hello.o
或者 gcc -c hello.s -o hello.o
查看 .c 代码的汇编代码可以用 gcc -S -g -o hello.s hello.c
查看 .o文件的汇编代码可以用 objdump -S hello.o > hello.s
2、静态链接
如果一些函数很通用,其他项目也要用,可以抽出来,单独做成库,这样就不用写重复代码了。
生成静态库:
gcc -c xxx.c -o xxx.o
ar -rcs libxxx.a xxx.o
链接:
gcc main.o -L . -lxxx -o s.out
可以查看静态库包文件中含了哪些文件:
$ ar -t libxxx.a
xxx.o
或
ar tv /usr/lib/gcc/x86_64-linux-gnu/8/libgcc.a
ar -x
可以从 *.a 解压出 *.o文件:
ar -x libxxx.a
静态链接的缺点:
一是浪费空间,每个可执行程序中都有一份所有需要的目标文件的副本;
二是更新比较麻烦,每当库函数的代码修改了,不仅要重新编译静态库,还需要重新链接所有用到该库的项目,以形成新的可执行程序。
优点就是执行的时候速度快一些。
3、动态链接
可执行文件较小,在程序运行时才将它们链接在一起形成一个完整的程序,不会在内存中存在多份副本。
生成动态库的命令:
gcc -c xxx.c -o xxx.o
gcc -fPIC -shared xxx.o -o libxxx.so
链接的命令和静态是一样的,不过动态链接后,运行时可能会报错
error while loading shared libraries: libxxx.so: cannot open shared object file: No such file or directory
因为找不到该动态库,需要把库文件所在目录添加到环境变量:
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
查询一个可执行文件依赖哪些动态库:
$ ldd a.out
linux-vdso.so.1 (0x00007ffd04588000)
libxxx.so => ./libxxx.so (0x00007f3989050000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3988c5f000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3989454000)
查看某个函数是否在某个库文件中
$ nm -D /lib/x86_64-linux-gnu/libc.so.6 |grep xxx_1
$ nm -D libxxx.so |grep xxx_1
000000000000065a T xxx_1
除了 nm
命令,还可以使用 readelf
、 objdump
或 strings
命令,这些命令都属于 GNU binutils 工具集。:
$ readelf -a libxxx.so |grep xxx_1
8: 000000000000065a 31 FUNC GLOBAL DEFAULT 12 xxx_1
55: 000000000000065a 31 FUNC GLOBAL DEFAULT 12 xxx_1
$ objdump -tT libxxx.so |grep xxx_1
000000000000065a g F .text 000000000000001f xxx_1
000000000000065a g DF .text 000000000000001f Base xxx_1
$ strings libxxx.so |grep xxx_1
xxx_1
xxx_1
xxx_1
比如我们在嵌入式编程,改了一个函数,想看看效果,又不想完整编译、重新升级,这时候替换板子上的 .so 文件会比较快,这时候就需要确认一下这个函数属于哪一个库文件。
在 /usr/lib/
、 /usr/local/lib/
或 /lib/
目录有很多 *.a 和 *.so 文件,可以用这些命令来测试玩一玩。