<p><strong>环境:</strong></p>
<ul><li>centos7.6</li><li>gcc 4.8.5</li></ul>
<h2>1. 从一个test.c到test.out</h2>
<p>这里实验的环境是 linux,linux的可执行文件默认后缀名是.out。</p>
<p>先看下面代码:</p>
<p><strong>test.c</strong></p>
- #include <stdio.h>
- int main()
- {
- printf("ok\n");
- return 0;
- }
复制代码
<p>我们,首先使用gcc test.c --save-temps -o test.out将它编译为test.out,并保留痕迹,如下:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/31858aa72511a1752201e8da90f99a55.png" alt="在这里插入图片描述" />
</p>
<p>--save-temps 命令可以保留整个编译的过程痕迹。</p>
<p>根据上面的痕迹,我们直接上图解释其过程:</p>
<p>
<img src="https://i-blog.csdnimg.cn/blog_migrate/aaebd632377cec8d86018966fd0750ff.png" alt="在这里插入图片描述" />
</p>
<h3>第一步:预处理</h3>
<p>预处理器先对test.c进行预处理,就是将里面#include...、#ifdef...等内容处理掉,处理完后,里面将不再有#include...等内容,最后生成test.i。关于预处理的内容,可参考《c:预处理指令(#include、#define、#if等)》。<br /> 总之,生成的文件内容,大概如下:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/5e8810826d2933f929312e3edc4123f0.png" alt="在这里插入图片描述" />
</p>
<h3>第二步:编译</h3>
<p>在这一步,gcc就将test.i进行词法分析,优化,最终转成汇编文件test.s,注意,test.s仍然是文本,大概如下图:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/dd1aadad63a64d6bdbbdb729591e2848.png" alt="在这里插入图片描述" />
</p>
<h3>第三步:汇编</h3>
<p>在这一步,汇编器就将test.s翻译成了二进制格式的指令,输出为test.o,它是ELF格式的二进制文件(后面说ELF文件格式)。<br /> test.o又称为可冲定位文件,我们通过file可观察到:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/f24d2cbf640d19d1c7e5c020dd9e66f3.png" alt="在这里插入图片描述" />
</p>
<h3>第四步:链接</h3>
<p>在这一步,链接器就将test.o文件与其引用资源做链接,主要是与其他引用的资源进行整合,重新分配内存地址,最终生成test.out,使用file观察到:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/0a056f96bd8633bcd89fd2e179bf45cd.png" alt="在这里插入图片描述" />
</p>
<h2>2. GCC是一个编译驱动器</h2>
<p>在上面4个步骤中,我们分别提到:预处理器、编译器、汇编器、链接器,这四个有对应专门的程序,如:</p>
<ul><li>预处理器:/usr/bin/cpp</li><li>编译器:/usr/bin/cc或/usr/bin/c++(可能是这两个)</li><li>汇编器:/usr/bin/as</li><li>链接器:/usr/bin/ld</li></ul>
<p>如果我们是window环境,看的就更清楚了:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/c901a16dd0745739f67d1e1ea42ebab2.png" alt="在这里插入图片描述" />
<br /> 既然,gcc是编译驱动器,那么脱离gcc命令,我们自然也能一步步编译,比如:<br /> cpp test.c test.i :进行预处理<br /> as test.s -o test.o:进行汇编</p>
<p>为什么没有列举其他两个过程?因为实验的时候遇到各种错误,放弃了。。。</p>
<p>现在,我们知道了编译的四个过程,那么我们能控制在哪一步停下来吗?<br /> 当然了,我们还是回归gcc命令:</p>
<ul><li>只进行预处理 test.c => test.i<br /> gcc -E test.c -o test.i</li><li>预处理并编译 test.c => test.s<br /> gcc -S test.c -o test.s</li><li>预处理、编译并汇编 test.c => test.o<br /> gcc -c test.c -o test.o</li><li>整个流程,输出可执行文件 test.c => test.out<br /> gcc test.c -o test.out</li></ul>
<h2>3. 关于ELF文件</h2>
<p>上面我们提到 test.o 和 test.out 都是ELF 格式的二进制文件。那么什么是ELF文件呢?</p>
<p>直接看百度百科的介绍:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/d049746e74b84497ba72ad0ea5845ddc.png" alt="在这里插入图片描述" />
<br /> 也就是说我们关心的 ELF可以表示4类文件:</p>
<ul><li>目标代码:test.o</li><li>可执行文件:test.out</li><li>动态库:test.so</li><li>核心转储文件(用的较少,一般是辅助调试的)</li></ul>
<p>那么ELF内部的格式是怎样的呢?</p>
<p>还是看百度百科:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/cb4b4da27948f28d409dfbbc12b2cd56.png" alt="在这里插入图片描述" />
</p>
<p>再往深处,我们就不研究了,知道个大概就行。</p>
<h2>4. 说说链接</h2>
<p>对上面的4个过程,我们疑问最大的应该是 链接,不知道为什么需要链接。。。</p>
<p>链接其实有两个目的:</p>
<ul><li>
<ol><li>布局<br /> 假设,我们有两个文件test.c和libadd.c并且test.c调用了libadd.c的函数,那么,编译时,编译器先分别生成test.o和libadd.o目标文件。因为它们是分别编译,所以test.o和libadd.o里面汇编指令涉及到的地址都认为是从0开始,即,它们之间互不认识。<br /> 而链接器的布局就是要把它们的地址空间合起来,防止重叠。</li></ol> </li><li>
<ol start="2"><li>重定位<br /> 还是假设上面的两个文件 test.c和libadd.c。我们知道 test.c调用函数的时候只是调用了int add(int x,int y) 这个声明而已,至于这个函数的具体实现在哪?test.o里面是没有的,所以test.o里面涉及到调用的地方是callq 0x0000,即:不知道这个函数的地址,就先填充0。<br /> 所以,链接器就要去帮test.o去找这个函数的实现,而libadd.o里恰好有这个函数的声明,那么就把libadd.o里的地址给test.o好了。</li></ol> </li></ul>
<p>这是链接器的两大目的,上面我简化着说,实际很复杂。</p>
<p>这里举的例子属于静态链接,另外还有动态链接(比如我们调用printf等标准函数就是),动态链接里面存的是库装载器的地址。</p>
<h2>5. 动态库和静态库</h2>
<p>上面我们在链接时也提到了静态链接和动态链接。所谓静态链接就是将引用的库也一并拷贝过来,而动态链接就不需要拷贝。所以,动态链接比静态链接应用的多很多。</p>
<p>那明白了动态链接和静态链接后,我们就应该知道动态库和静态库了吧。<br /> 现在我们就来实验下:</p>
<h3>5.1 生成静态库</h3>
<p>所谓的静态库就是将编译好的目标代码(如:libadd.o、libsub.o)打成一个压缩包而已,一般后缀名是*.a。</p>
<p>首先,准备三个文件:</p>
<p><strong>test.c</strong></p>
- #include <stdio.h>
- int add(int x,int y);
- int sub(int x,int y);
- int main()
- {
- int x=20,y=10;
- printf("x+y=%d\n",add(x,y));
- printf("x-y=%d\n",sub(x,y));
- printf("ok\n");
- return 0;
- }
复制代码
<p><strong>libadd.c</strong></p>
- int add(int x,int y)
- {
- return x+y;
- }
复制代码
<p><strong>libsub.c</strong></p>
- int sub(int x,int y)
- {
- return x-y;
- }
复制代码
<p>现在,我们分别编译它们:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/b83f2ff23cc11bea38f657d11125feb4.png" alt="在这里插入图片描述" />
<br /> 现在,让我们把 libadd.o 和 libsub.o做成静态库:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/a2277a428600a99401fc8cb285ef0682.png" alt="在这里插入图片描述" />
</p>
<p>ar rcs ... 中,r表示replace,c表示create</p>
<h3>5.2 调用静态库编译</h3>
<p>接上面,我们将test.o和libaddsub.a生成 test.out:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/4f3abd0eb3230dcb63dfb01622383d1d.png" alt="在这里插入图片描述" />
</p>
<h3>5.3 使用动态库</h3>
<p>注意:动态库是一个ELF格式的二进制文件,不是压缩包,后缀名是*.so</p>
<p>上面,我们是将libadd.c和libsub.c生成了静态库,现在我们让它们分别生成动态库:</p>
- # 生成 libadd.so
- gcc -shared libadd.c -o libadd.so
- # 生成 libsub.so
- gcc -shared libsub.c -o libsub.so
复制代码
<p>现在,让我们使用动态库编译可执行文件:</p>
- # 生成 test.out
- gcc test.c libadd.so libsub.so -o test.out
复制代码
<p>但当我们执行test.out时,却大失所望:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/6ad6f896994a6014ce4bf0685e93e46f.png" alt="在这里插入图片描述" />
<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/72defa08662011ebf9c8763fc42bc945.png" alt="在这里插入图片描述" />
</p>
<p>为什么会这样呢?当前目录下不是有libadd.so 吗?</p>
<p>这就要说linux加载动态库的原理了:</p>
<p>linux会根据配置从指定路径下找动态库,而不是当前目录,那么这个配置在哪呢?<br /> 在 /etc/ld.so.conf:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/d9f8f8a489ca8dbab40c1283ac8c747a.png" alt="在这里插入图片描述" />
<br /> 可以看到,这个文件里指定了从 etc/ld.so.conf.d/*里面找<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/82167ab8bd4571d33c533bbac8ed6c51.png" alt="在这里插入图片描述" />
<br /> 可以看到,最终只在 /usr/lib64/mysql下找(注意:还有默认的 /lib64等没有列出)。</p>
<p>那么,当前已经找到多少动态库呢?<br /> 可以通过 ldconfig -p查看:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/704c184af0353cc7a9288c90b3f79e37.png" alt="在这里插入图片描述" />
</p>
<p>另外:/etc/ld.so.cache文件是动态库的缓存,运行 ldconfig命令可以强制更新/etc/ld.so.cache文件。</p>
<p>现在,我们知道了,要么我们把libadd.so和libsub.so放到/lib64等系统目录下,要么将libadd.so所在的目录配置到/ect/ld.so.conf.d目录下。这的确是一个解决方法。</p>
<p>但,我们还有另外一个解决办法,那就是使用LD_LIBRARY_PATH环境变量,如下:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/6596dd2fd5063f04ca0d9e0cf934e89f.png" alt="在这里插入图片描述" />
<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/a7f3e36dd9c5e0208bc59d01ab73622a.png" alt="在这里插入图片描述" />
</p>
<p>然后,我们可以把这个环境变量的配置放到 /etc/profile里面。</p>
<p>如果,我们不想在系统上留下什么痕迹,那么我们可以写一个脚本,内容如下:</p>
- current_dir=$(cd $(dirname $0); pwd)
- export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:current_dir
- ./test.out
复制代码
<p>效果如下:<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/49ea89a1eddf80abf5b1ede30e24e664.png" alt="在这里插入图片描述" />
<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/8ba53e014b017f9effdf6ad98c313f5e.png" alt="在这里插入图片描述" />
</p>
<h2>6. 对c语言编译的一些理解</h2>
<p>其实根据上面的编译过程,我们应该认识到,c语言编译整体分成两大步骤:</p>
<ul><li>所有单个c文件分别编译成目标代码*.o;</li><li>将多个*.o、静态库或动态库链接成可执行文件test.out</li></ul>
<p>所以,c语言编译的时候是,先单个编译,然后再整合资源。<br /> 所以,c语言中的单个c文件中可以没有某个api函数的实现,但如果要调用,就必须在调用前头先声明一下(全局变量也是一个意思)。</p>
<h2>7. gcc常用编译选项</h2>
<p>除了上面的编译命令,我们常用的还有<br /> gcc -Og test.c -o test.out</p>
<p>这里的 -g是生成调试用的信息(如果我们想调试的话,比如使用gdb调试);<br /> -O是优化选项。</p>
<h2>8. 补充gcc附带的其他命令</h2>
<h3>8.1 objdump</h3>
<h4>8.1.1 显示libaddsub.a内信息</h4>
<p>
<img src="https://i-blog.csdnimg.cn/blog_migrate/2b9d178b7c8f5eecb1c97730e2647017.png" alt="在这里插入图片描述" />
</p>
<h4>8.1.2 显示libadd.o的反汇编信息</h4>
<p>
<img src="https://i-blog.csdnimg.cn/blog_migrate/44b22908389b52a6a93449a18735b027.png" alt="在这里插入图片描述" />
<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/5e4570847c7e38629aa77c4a891e9bcb.png" alt="在这里插入图片描述" />
</p>
<h4>8.1.3 显示符号表信息</h4>
<p>
<img src="https://i-blog.csdnimg.cn/blog_migrate/b17cb0f2f9110fdbf2966d61a52070e4.png" alt="在这里插入图片描述" />
<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/57fe9ebb3cf8c36f6276cf4c170b2fdd.png" alt="在这里插入图片描述" />
<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/22dc6d3744f783fe4f8af4389b6d34d7.png" alt="在这里插入图片描述" />
</p>
<p>关于objdump更多,参考:obdump -v 或 man objdump</p>
<h3>8.2 readelf</h3>
<p>上面,我们说 test.o、test.so、test.out 都是ELF格式的二进制文件,现在我们就用readelf去看看:</p>
<h4>8.2.1 显示elf的文件头信息</h4>
<p>
<img src="https://i-blog.csdnimg.cn/blog_migrate/64b776ea018919e1e53e1ae447931e04.png" alt="在这里插入图片描述" />
<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/62505b708b1ad19d3327713a8baadf94.png" alt="在这里插入图片描述" />
<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/41d0722d5f940e5adabda27e70d0b657.png" alt="在这里插入图片描述" />
</p>
<h4>8.2.2 显示程序头表信息</h4>
<p>
<img src="https://i-blog.csdnimg.cn/blog_migrate/fd6a15d3dbf5932a05434ae5184d7373.png" alt="在这里插入图片描述" />
<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/6857a967265063ee2163d5830ea78296.png" alt="在这里插入图片描述" />
<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/82661871ef67bfd70fb90b83d0659cae.png" alt="在这里插入图片描述" />
</p>