[C.C++] c:再说c语言编译过程

510 0
Honkers 2025-3-11 14:01:53 | 显示全部楼层 |阅读模式
<p><strong>环境&#xff1a;</strong></p>
<ul><li>centos7.6</li><li>gcc 4.8.5</li></ul>
<h2>1. 从一个test.c到test.out</h2>
<p>这里实验的环境是 linux&#xff0c;linux的可执行文件默认后缀名是.out。</p>
<p>先看下面代码&#xff1a;</p>
<p><strong>test.c</strong></p>
  1. #include &lt;stdio.h&gt;
  2. int main()
  3. {
  4.     printf(&#34;ok\n&#34;);
  5.     return 0;
  6. }
复制代码

<p>我们&#xff0c;首先使用gcc test.c --save-temps -o test.out将它编译为test.out&#xff0c;并保留痕迹&#xff0c;如下&#xff1a;<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/31858aa72511a1752201e8da90f99a55.png" alt="在这里插入图片描述" />
</p>

<p>--save-temps 命令可以保留整个编译的过程痕迹。</p>

<p>根据上面的痕迹&#xff0c;我们直接上图解释其过程&#xff1a;</p>
<p>
<img src="https://i-blog.csdnimg.cn/blog_migrate/aaebd632377cec8d86018966fd0750ff.png" alt="在这里插入图片描述" />
</p>
<h3>第一步&#xff1a;预处理</h3>
<p>预处理器先对test.c进行预处理&#xff0c;就是将里面#include...、#ifdef...等内容处理掉&#xff0c;处理完后&#xff0c;里面将不再有#include...等内容&#xff0c;最后生成test.i。关于预处理的内容&#xff0c;可参考《c&#xff1a;预处理指令&#xff08;#include、#define、#if等&#xff09;》。<br /> 总之&#xff0c;生成的文件内容&#xff0c;大概如下&#xff1a;<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/5e8810826d2933f929312e3edc4123f0.png" alt="在这里插入图片描述" />
</p>
<h3>第二步&#xff1a;编译</h3>
<p>在这一步&#xff0c;gcc就将test.i进行词法分析&#xff0c;优化&#xff0c;最终转成汇编文件test.s&#xff0c;注意&#xff0c;test.s仍然是文本&#xff0c;大概如下图&#xff1a;<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/dd1aadad63a64d6bdbbdb729591e2848.png" alt="在这里插入图片描述" />
</p>
<h3>第三步&#xff1a;汇编</h3>
<p>在这一步&#xff0c;汇编器就将test.s翻译成了二进制格式的指令&#xff0c;输出为test.o&#xff0c;它是ELF格式的二进制文件&#xff08;后面说ELF文件格式&#xff09;。<br /> test.o又称为可冲定位文件&#xff0c;我们通过file可观察到&#xff1a;<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/f24d2cbf640d19d1c7e5c020dd9e66f3.png" alt="在这里插入图片描述" />
</p>
<h3>第四步&#xff1a;链接</h3>
<p>在这一步&#xff0c;链接器就将test.o文件与其引用资源做链接&#xff0c;主要是与其他引用的资源进行整合&#xff0c;重新分配内存地址&#xff0c;最终生成test.out&#xff0c;使用file观察到&#xff1a;<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/0a056f96bd8633bcd89fd2e179bf45cd.png" alt="在这里插入图片描述" />
</p>
<h2>2. GCC是一个编译驱动器</h2>
<p>在上面4个步骤中&#xff0c;我们分别提到&#xff1a;预处理器、编译器、汇编器、链接器&#xff0c;这四个有对应专门的程序&#xff0c;如&#xff1a;</p>
<ul><li>预处理器&#xff1a;/usr/bin/cpp</li><li>编译器&#xff1a;/usr/bin/cc或/usr/bin/c&#43;&#43;&#xff08;可能是这两个&#xff09;</li><li>汇编器&#xff1a;/usr/bin/as</li><li>链接器&#xff1a;/usr/bin/ld</li></ul>
<p>如果我们是window环境&#xff0c;看的就更清楚了&#xff1a;<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/c901a16dd0745739f67d1e1ea42ebab2.png" alt="在这里插入图片描述" />
<br /> 既然&#xff0c;gcc是编译驱动器&#xff0c;那么脱离gcc命令&#xff0c;我们自然也能一步步编译&#xff0c;比如&#xff1a;<br /> cpp test.c test.i &#xff1a;进行预处理<br /> as test.s -o test.o&#xff1a;进行汇编</p>

<p>为什么没有列举其他两个过程&#xff1f;因为实验的时候遇到各种错误&#xff0c;放弃了。。。</p>

<p>现在&#xff0c;我们知道了编译的四个过程&#xff0c;那么我们能控制在哪一步停下来吗&#xff1f;<br /> 当然了&#xff0c;我们还是回归gcc命令&#xff1a;</p>
<ul><li>只进行预处理 test.c &#61;&gt; test.i<br /> gcc -E test.c -o test.i</li><li>预处理并编译 test.c &#61;&gt; test.s<br /> gcc -S test.c -o test.s</li><li>预处理、编译并汇编 test.c &#61;&gt; test.o<br /> gcc -c test.c -o test.o</li><li>整个流程&#xff0c;输出可执行文件 test.c &#61;&gt; test.out<br /> gcc test.c -o test.out</li></ul>
<h2>3. 关于ELF文件</h2>
<p>上面我们提到 test.o 和 test.out 都是ELF 格式的二进制文件。那么什么是ELF文件呢&#xff1f;</p>
<p>直接看百度百科的介绍&#xff1a;<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/d049746e74b84497ba72ad0ea5845ddc.png" alt="在这里插入图片描述" />
<br /> 也就是说我们关心的 ELF可以表示4类文件&#xff1a;</p>
<ul><li>目标代码&#xff1a;test.o</li><li>可执行文件&#xff1a;test.out</li><li>动态库&#xff1a;test.so</li><li>核心转储文件&#xff08;用的较少&#xff0c;一般是辅助调试的&#xff09;</li></ul>
<p>那么ELF内部的格式是怎样的呢&#xff1f;</p>
<p>还是看百度百科&#xff1a;<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/cb4b4da27948f28d409dfbbc12b2cd56.png" alt="在这里插入图片描述" />
</p>
<p>再往深处&#xff0c;我们就不研究了&#xff0c;知道个大概就行。</p>
<h2>4. 说说链接</h2>
<p>对上面的4个过程&#xff0c;我们疑问最大的应该是 链接&#xff0c;不知道为什么需要链接。。。</p>
<p>链接其实有两个目的&#xff1a;</p>
<ul><li>
  <ol><li>布局<br /> 假设&#xff0c;我们有两个文件test.c和libadd.c并且test.c调用了libadd.c的函数&#xff0c;那么&#xff0c;编译时&#xff0c;编译器先分别生成test.o和libadd.o目标文件。因为它们是分别编译&#xff0c;所以test.o和libadd.o里面汇编指令涉及到的地址都认为是从0开始&#xff0c;即&#xff0c;它们之间互不认识。<br /> 而链接器的布局就是要把它们的地址空间合起来&#xff0c;防止重叠。</li></ol> </li><li>
  <ol start="2"><li>重定位<br /> 还是假设上面的两个文件 test.c和libadd.c。我们知道 test.c调用函数的时候只是调用了int add(int x,int y) 这个声明而已&#xff0c;至于这个函数的具体实现在哪&#xff1f;test.o里面是没有的&#xff0c;所以test.o里面涉及到调用的地方是callq 0x0000&#xff0c;即&#xff1a;不知道这个函数的地址&#xff0c;就先填充0。<br /> 所以&#xff0c;链接器就要去帮test.o去找这个函数的实现&#xff0c;而libadd.o里恰好有这个函数的声明&#xff0c;那么就把libadd.o里的地址给test.o好了。</li></ol> </li></ul>
<p>这是链接器的两大目的&#xff0c;上面我简化着说&#xff0c;实际很复杂。</p>

<p>这里举的例子属于静态链接&#xff0c;另外还有动态链接(比如我们调用printf等标准函数就是)&#xff0c;动态链接里面存的是库装载器的地址。</p>

<h2>5. 动态库和静态库</h2>
<p>上面我们在链接时也提到了静态链接和动态链接。所谓静态链接就是将引用的库也一并拷贝过来&#xff0c;而动态链接就不需要拷贝。所以&#xff0c;动态链接比静态链接应用的多很多。</p>
<p>那明白了动态链接和静态链接后&#xff0c;我们就应该知道动态库和静态库了吧。<br /> 现在我们就来实验下&#xff1a;</p>
<h3>5.1 生成静态库</h3>

<p>所谓的静态库就是将编译好的目标代码&#xff08;如&#xff1a;libadd.o、libsub.o&#xff09;打成一个压缩包而已&#xff0c;一般后缀名是*.a。</p>

<p>首先&#xff0c;准备三个文件&#xff1a;</p>
<p><strong>test.c</strong></p>
  1. #include &lt;stdio.h&gt;
  2. int add(int x,int y);
  3. int sub(int x,int y);
  4. int main()
  5. {
  6.     int x&#61;20,y&#61;10;
  7.     printf(&#34;x&#43;y&#61;%d\n&#34;,add(x,y));
  8.     printf(&#34;x-y&#61;%d\n&#34;,sub(x,y));
  9.     printf(&#34;ok\n&#34;);
  10.     return 0;
  11. }
复制代码

<p><strong>libadd.c</strong></p>
  1. int add(int x,int y)
  2. {
  3.     return x&#43;y;
  4. }
复制代码

<p><strong>libsub.c</strong></p>
  1. int sub(int x,int y)
  2. {
  3.     return x-y;
  4. }
复制代码

<p>现在&#xff0c;我们分别编译它们&#xff1a;<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/b83f2ff23cc11bea38f657d11125feb4.png" alt="在这里插入图片描述" />
<br /> 现在&#xff0c;让我们把 libadd.o 和 libsub.o做成静态库&#xff1a;<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/a2277a428600a99401fc8cb285ef0682.png" alt="在这里插入图片描述" />
</p>

<p>ar rcs ... 中&#xff0c;r表示replace&#xff0c;c表示create</p>

<h3>5.2 调用静态库编译</h3>
<p>接上面&#xff0c;我们将test.o和libaddsub.a生成 test.out&#xff1a;<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/4f3abd0eb3230dcb63dfb01622383d1d.png" alt="在这里插入图片描述" />
</p>
<h3>5.3 使用动态库</h3>

<p>注意&#xff1a;动态库是一个ELF格式的二进制文件&#xff0c;不是压缩包&#xff0c;后缀名是*.so</p>

<p>上面&#xff0c;我们是将libadd.c和libsub.c生成了静态库&#xff0c;现在我们让它们分别生成动态库&#xff1a;</p>
  1. # 生成 libadd.so
  2. gcc -shared libadd.c -o libadd.so
  3. # 生成 libsub.so
  4. gcc -shared libsub.c -o libsub.so
复制代码

<p>现在&#xff0c;让我们使用动态库编译可执行文件&#xff1a;</p>
  1. # 生成 test.out
  2. gcc test.c libadd.so libsub.so -o test.out
复制代码

<p>但当我们执行test.out时&#xff0c;却大失所望&#xff1a;<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>为什么会这样呢&#xff1f;当前目录下不是有libadd.so 吗?</p>
<p>这就要说linux加载动态库的原理了&#xff1a;</p>
<p>linux会根据配置从指定路径下找动态库&#xff0c;而不是当前目录&#xff0c;那么这个配置在哪呢&#xff1f;<br /> 在 /etc/ld.so.conf&#xff1a;<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/d9f8f8a489ca8dbab40c1283ac8c747a.png" alt="在这里插入图片描述" />
<br /> 可以看到&#xff0c;这个文件里指定了从 etc/ld.so.conf.d/*里面找<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/82167ab8bd4571d33c533bbac8ed6c51.png" alt="在这里插入图片描述" />
<br /> 可以看到&#xff0c;最终只在 /usr/lib64/mysql下找&#xff08;注意&#xff1a;还有默认的 /lib64等没有列出&#xff09;。</p>
<p>那么&#xff0c;当前已经找到多少动态库呢&#xff1f;<br /> 可以通过 ldconfig -p查看&#xff1a;<br />
<img src="https://i-blog.csdnimg.cn/blog_migrate/704c184af0353cc7a9288c90b3f79e37.png" alt="在这里插入图片描述" />
</p>

<p>另外&#xff1a;/etc/ld.so.cache文件是动态库的缓存&#xff0c;运行 ldconfig命令可以强制更新/etc/ld.so.cache文件。</p>

<p>现在&#xff0c;我们知道了&#xff0c;要么我们把libadd.so和libsub.so放到/lib64等系统目录下&#xff0c;要么将libadd.so所在的目录配置到/ect/ld.so.conf.d目录下。这的确是一个解决方法。</p>
<p>但&#xff0c;我们还有另外一个解决办法&#xff0c;那就是使用LD_LIBRARY_PATH环境变量&#xff0c;如下&#xff1a;<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>然后&#xff0c;我们可以把这个环境变量的配置放到 /etc/profile里面。</p>
<p>如果&#xff0c;我们不想在系统上留下什么痕迹&#xff0c;那么我们可以写一个脚本&#xff0c;内容如下&#xff1a;</p>
  1. current_dir&#61;$(cd $(dirname $0); pwd)
  2. export LD_LIBRARY_PATH&#61;$LD_LIBRARY_PATH:current_dir
  3. ./test.out
复制代码

<p>效果如下&#xff1a;<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>其实根据上面的编译过程&#xff0c;我们应该认识到&#xff0c;c语言编译整体分成两大步骤&#xff1a;</p>
<ul><li>所有单个c文件分别编译成目标代码*.o&#xff1b;</li><li>将多个*.o、静态库或动态库链接成可执行文件test.out</li></ul>
<p>所以&#xff0c;c语言编译的时候是&#xff0c;先单个编译&#xff0c;然后再整合资源。<br /> 所以&#xff0c;c语言中的单个c文件中可以没有某个api函数的实现&#xff0c;但如果要调用&#xff0c;就必须在调用前头先声明一下&#xff08;全局变量也是一个意思&#xff09;。</p>
<h2>7. gcc常用编译选项</h2>
<p>除了上面的编译命令&#xff0c;我们常用的还有<br /> gcc -Og test.c -o test.out</p>
<p>这里的 -g是生成调试用的信息&#xff08;如果我们想调试的话&#xff0c;比如使用gdb调试&#xff09;&#xff1b;<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更多&#xff0c;参考&#xff1a;obdump -v 或 man objdump</p>
<h3>8.2 readelf</h3>
<p>上面&#xff0c;我们说 test.o、test.so、test.out 都是ELF格式的二进制文件&#xff0c;现在我们就用readelf去看看&#xff1a;</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>
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Honkers

荣誉红客

关注
  • 4009
    主题
  • 36
    粉丝
  • 0
    关注
这家伙很懒,什么都没留下!

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2025 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行