c:再说c语言编译过程

环境:

  • centos7.6
  • gcc 4.8.5

1. 从一个test.ctest.out

这里实验的环境是 linuxlinux的可执行文件默认后缀名是.out

先看下面代码:

test.c

#include <stdio.h>

int main()
{
    
    
    printf("ok\n");
    return 0;
}

我们,首先使用gcc test.c --save-temps -o test.out将它编译为test.out,并保留痕迹,如下:
在这里插入图片描述

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

根据上面的痕迹,我们直接上图解释其过程:

在这里插入图片描述

第一步:预处理

预处理器先对test.c进行预处理,就是将里面#include...#ifdef...等内容处理掉,处理完后,里面将不再有#include...等内容,最后生成test.i。关于预处理的内容,可参考《c:预处理指令(#include、#define、#if等)》
总之,生成的文件内容,大概如下:
在这里插入图片描述

第二步:编译

在这一步,gcc就将test.i进行词法分析,优化,最终转成汇编文件test.s,注意,test.s仍然是文本,大概如下图:
在这里插入图片描述

第三步:汇编

在这一步,汇编器就将test.s翻译成了二进制格式的指令,输出为test.o,它是ELF格式的二进制文件(后面说ELF文件格式)。
test.o又称为可冲定位文件,我们通过file可观察到:
在这里插入图片描述

第四步:链接

在这一步,链接器就将test.o文件与其引用资源做链接,主要是与其他引用的资源进行整合,重新分配内存地址,最终生成test.out,使用file观察到:
在这里插入图片描述

2. GCC是一个编译驱动器

在上面4个步骤中,我们分别提到:预处理器编译器汇编器链接器,这四个有对应专门的程序,如:

  • 预处理器:/usr/bin/cpp
  • 编译器:/usr/bin/cc/usr/bin/c++(可能是这两个)
  • 汇编器:/usr/bin/as
  • 链接器:/usr/bin/ld

如果我们是window环境,看的就更清楚了:
在这里插入图片描述
既然,gcc是编译驱动器,那么脱离gcc命令,我们自然也能一步步编译,比如:
cpp test.c test.i :进行预处理
as test.s -o test.o:进行汇编

为什么没有列举其他两个过程?因为实验的时候遇到各种错误,放弃了。。。

现在,我们知道了编译的四个过程,那么我们能控制在哪一步停下来吗?
当然了,我们还是回归gcc命令:

  • 只进行预处理 test.c => test.i
    gcc -E test.c -o test.i
  • 预处理并编译 test.c => test.s
    gcc -S test.c -o test.s
  • 预处理、编译并汇编 test.c => test.o
    gcc -c test.c -o test.o
  • 整个流程,输出可执行文件 test.c => test.out
    gcc test.c -o test.out

3. 关于ELF文件

上面我们提到 test.otest.out 都是ELF 格式的二进制文件。那么什么是ELF文件呢?

直接看百度百科的介绍:
在这里插入图片描述
也就是说我们关心的 ELF可以表示4类文件:

  • 目标代码:test.o
  • 可执行文件:test.out
  • 动态库:test.so
  • 核心转储文件(用的较少,一般是辅助调试的)

那么ELF内部的格式是怎样的呢?

还是看百度百科:
在这里插入图片描述

再往深处,我们就不研究了,知道个大概就行。

4. 说说链接

对上面的4个过程,我们疑问最大的应该是 链接,不知道为什么需要链接。。。

链接其实有两个目的:

    1. 布局
      假设,我们有两个文件test.clibadd.c并且test.c调用了libadd.c的函数,那么,编译时,编译器先分别生成test.olibadd.o目标文件。因为它们是分别编译,所以test.olibadd.o里面汇编指令涉及到的地址都认为是从0开始,即,它们之间互不认识。
      而链接器的布局就是要把它们的地址空间合起来,防止重叠。
    1. 重定位
      还是假设上面的两个文件 test.clibadd.c。我们知道 test.c调用函数的时候只是调用了int add(int x,int y) 这个声明而已,至于这个函数的具体实现在哪?test.o里面是没有的,所以test.o里面涉及到调用的地方是callq 0x0000,即:不知道这个函数的地址,就先填充0。
      所以,链接器就要去帮test.o去找这个函数的实现,而libadd.o里恰好有这个函数的声明,那么就把libadd.o里的地址给test.o好了。

这是链接器的两大目的,上面我简化着说,实际很复杂。

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

5. 动态库和静态库

上面我们在链接时也提到了静态链接动态链接。所谓静态链接就是将引用的库也一并拷贝过来,而动态链接就不需要拷贝。所以,动态链接静态链接应用的多很多。

那明白了动态链接静态链接后,我们就应该知道动态库静态库了吧。
现在我们就来实验下:

5.1 生成静态库

所谓的静态库就是将编译好的目标代码(如:libadd.olibsub.o)打成一个压缩包而已,一般后缀名是*.a

首先,准备三个文件:

test.c

#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;
}

libadd.c

int add(int x,int y)
{
    
    
    return x+y;
}

libsub.c

int sub(int x,int y)
{
    
    
    return x-y;
}

现在,我们分别编译它们:
在这里插入图片描述
现在,让我们把 libadd.olibsub.o做成静态库:
在这里插入图片描述

ar rcs ... 中,r表示replace,c表示create

5.2 调用静态库编译

接上面,我们将test.olibaddsub.a生成 test.out
在这里插入图片描述

5.3 使用动态库

注意:动态库是一个ELF格式的二进制文件,不是压缩包,后缀名是*.so

上面,我们是将libadd.clibsub.c生成了静态库,现在我们让它们分别生成动态库:

# 生成 libadd.so
gcc -shared libadd.c -o libadd.so
# 生成 libsub.so
gcc -shared libsub.c -o libsub.so

现在,让我们使用动态库编译可执行文件:

# 生成 test.out
gcc test.c libadd.so libsub.so -o test.out

但当我们执行test.out时,却大失所望:
在这里插入图片描述
在这里插入图片描述

为什么会这样呢?当前目录下不是有libadd.so 吗?

这就要说linux加载动态库的原理了:

linux会根据配置从指定路径下找动态库,而不是当前目录,那么这个配置在哪呢?
/etc/ld.so.conf
在这里插入图片描述
可以看到,这个文件里指定了从 etc/ld.so.conf.d/*里面找
在这里插入图片描述
可以看到,最终只在 /usr/lib64/mysql下找(注意:还有默认的 /lib64等没有列出)。

那么,当前已经找到多少动态库呢?
可以通过 ldconfig -p查看:
在这里插入图片描述

另外:/etc/ld.so.cache文件是动态库的缓存,运行 ldconfig命令可以强制更新/etc/ld.so.cache文件。

现在,我们知道了,要么我们把libadd.solibsub.so放到/lib64等系统目录下,要么将libadd.so所在的目录配置到/ect/ld.so.conf.d目录下。这的确是一个解决方法。

但,我们还有另外一个解决办法,那就是使用LD_LIBRARY_PATH环境变量,如下:
在这里插入图片描述
在这里插入图片描述

然后,我们可以把这个环境变量的配置放到 /etc/profile里面。

如果,我们不想在系统上留下什么痕迹,那么我们可以写一个脚本,内容如下:

current_dir=$(cd $(dirname $0); pwd)
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:current_dir
./test.out

效果如下:
在这里插入图片描述
在这里插入图片描述

6. 对c语言编译的一些理解

其实根据上面的编译过程,我们应该认识到,c语言编译整体分成两大步骤:

  • 所有单个c文件分别编译成目标代码*.o
  • 将多个*.o、静态库或动态库链接成可执行文件test.out

所以,c语言编译的时候是,先单个编译,然后再整合资源。
所以,c语言中的单个c文件中可以没有某个api函数的实现,但如果要调用,就必须在调用前头先声明一下(全局变量也是一个意思)。

7. gcc常用编译选项

除了上面的编译命令,我们常用的还有
gcc -Og test.c -o test.out

这里的 -g是生成调试用的信息(如果我们想调试的话,比如使用gdb调试);
-O是优化选项。

8. 补充gcc附带的其他命令

8.1 objdump

8.1.1 显示libaddsub.a内信息

在这里插入图片描述

8.1.2 显示libadd.o的反汇编信息

在这里插入图片描述
在这里插入图片描述

8.1.3 显示符号表信息

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

关于objdump更多,参考:obdump -vman objdump

8.2 readelf

上面,我们说 test.otest.sotest.out 都是ELF格式的二进制文件,现在我们就用readelf去看看:

8.2.1 显示elf的文件头信息

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8.2.2 显示程序头表信息

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/u010476739/article/details/127384988