源码下载地址:百度网盘,提取码:cat1 。
通过前面的文章分析,读者应该已经对 可执行文件 ELF 的格式有一定了解,本文主要讲解,链接器 ld
是如何把 main.o
跟 sum.o
链接在一起的。
先执行以下命令,编译出来 main.o
跟 sum.o
.
gcc -c -o main.o main.c
gcc -c -o sum.o sum.c
为了方便读者对照, main.o
跟 sum.o
这两个文件可以在 百度网盘 下载,提取码:q2sy
gcc 的编译过程其实比较复杂,会经历 词法分析,语法分析,中间代码生成,但是这些东西本文不讲,暂时把他们理解成一个黑盒子就行,他们最后都是生成 main.o
跟 sum.o
,这两个 .o 文件是二进制文件,他们也是 ELF 格式的,所以也需要用 xelfviewer 软件 查看。
从上图可以看到, main.o
的符号其实是 比 main
文件少很多的,因为 链接器 还会把一些 操作系统 运行时的需要的一些符号加进来。
Ubuntu18 环境下的 链接器是 ld
,实际上用 gcc
命令也能链接,只是我上面故意 使用了 -c
选项不进行链接,只是编译。gcc
的代码太多,不利于分析链接器的实现。讲一个扩展知识,gcc
命令 里面是调 collect2
命令来链接的,而 collect2
是对 ld
进行了一些封装。
ld
链接 main.o
跟 sum.o
的命令如下:
ld -o main2 \
-dynamic-linker /lib64/ld-linux-x86-64.so.2 \
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o \
/usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/7 \
-L/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib -L/lib/x86_64-linux-gnu \
-L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/7/../../.. \
main.o sum.o \
-lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s \
--pop-state /usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o
实际上是有非常多的库链接进去。由于 系统自带 的 ld
命令没有符号表,不好用 gdb
调试。所以我们需要自己用 源码 编译出来 ld
命令。
ld
命令是在 binutils
里面的,所以打开 binutils 网站。如下:
可以看到 有很多命令行工具,大多数都是 用 BFD 库跟 opcodes 库实现的。binutils
是一个集成工具,他里面的 ld
,as
,nm
的版本号都是统一的
为了方便演示,我选择了一个 跟我 机器 版本一样的 ld
源码,ld 2.3.0 版本。下载之后,目录如下:
现在开始编译:
./configure --prefix=/usr/local/binutils_2.3.0
make -j48
编译成功之后,就可以看到 相关的文件,上面我没有执行 make install 来转移编译好的 ld
文件走。因为我想在源码目录运行 ld
,对于 clion
配置来说会方便一些。
通过研究他的 makefile
文件,我知道 生成的 ld
可执行文件的路径是 ld/ld-new
,用 新的 链接器再链接一下之前的 main.o
跟 sum.o
,命令如下:
/home/ubuntu/Documents/binutils-2.30/ld/ld-new -o main-new \
-dynamic-linker /lib64/ld-linux-x86-64.so.2 \
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o \
/usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/7 \
-L/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib -L/lib/x86_64-linux-gnu \
-L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/7/../../.. \
main.o sum.o \
-lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s \
--pop-state /usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o
从上图可以看到,新的 链接器 ld-new
是没有问题的。
下面开始讲解如何 配置 clion
来调试,用 gdb
也是可以的,现在已经有符合表,只是不太直观。我的环境是 ubuntu18,clion
是2021年版本的。下面开始操作:
1,点击右上角的 Add Configuration
,添加配置。
2,点击 + 按钮,选择 Makefile Target
3,添加两个 make 规则,一个是 make clean
(清除项目),第二个是 make
(编译生成可执行文件ld),第二个规则的 target 不填就行了,如下图:
现在我们把 ld-new
删掉,再执行一次 configure
,再点击 build-ld
这个按钮,看看能不能顺利编译出 ld-new
。
PS:注意,这里一定要先执行 configure
,再执行 make
,要不生成的 ld-new
有问题。
从上图可以看到,正常编译出 ld-new
了。
4,现在可以添加 Makefile Application
了,如下图:
上图有几个注意的点。
1,如果没有 target 显示,点右边的设置按钮,随便加一个,再返回来,他就会跑出来 all 这个target。这个可能是 clion 的bug。
2,Program arguments 就是 ld 后面的参数,这个非常多,需要注意换行。为了方便读者,我直接贴没有换行符的参数,复制进去clion即可。
-o main-new -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/7 -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/7/../../.. main.o sum.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o
3,/home/ubuntu/CLionProjects/c-test/
这个目录里面有 main.o
跟 sum.o
现在 clion
调试环境准备完毕了,在 ldmain.c
的 main
函数打一个断点,如下:
我个人觉得,链接器其实做的活,是脏活累活,跟CURD没什么两样。不同的平台有不同的运行文件格式,ld
就是解析这些格式,合并各个 .o
文件,链接器干的活就是兼容。基本上没啥深度。不过熟悉 ld
源码也是有好处的,可以在编译一些项目出问题的时候,快速找到解决方法,例如 undefined reference to xxx。
相关阅读:
由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1,QQ:2338195090。