C语言学习-探索编译过程

    技术2022-07-10  129

    编译环境:GNU

    前言

    本渣最近因为业务需求,需要写c语言,遇到了一些编译链接问题,整的我心态爆炸,因此抽出时间来好好研究以下C语言的编译流程。

    概览

    如下图所示,源文件编译成可执行文件的流程如下:

    预编译

    预编译的文件主要是对include指令进行处理,换句话说include本身就是一个预编译处理指令。其做用就是将include中的文件内容拷贝到当前文件。我们做一个简单的实验

    #include <stdio.h> #include <stdlib.h> int main() { printf("hello world!"); return 0; }

    如上面所说,include会将stdio与stdlib中的内容拷贝到当前文件。

    # 1 "hello.c" # 1 "<built-in>" # 1 "<command-line>" # 31 "<command-line>" # 1 "/usr/include/stdc-predef.h" 1 3 4 # 32 "<command-line>" 2 # 1 "hello.c" ......

    在这个过程中,主要进行如下:

    (1)将所有的#define删除,并且展开所有的宏定义。说白了就是字符替换 (2)处理所有的条件编译指令,#ifdef #ifndef #endif等,就是带#的那些 (3)处理#include,将#include指向的文件插入到该行处 (4)删除所有注释 (5)添加行号和文件标示,这样的在调试和编译出错的时候才知道是是哪个文件的哪一行 (6)保留#pragma编译器指令,因为编译器需要使用它们。

    编译

    编译的过程就是将高级语言翻译成低级语言的过程。这个过程根数据库中的解析sql命令的过程非常相似。 (1)词法分析, (2)语法分析 (3)语义分析 (4)优化后生成相应的汇编代码

    通过gcc -S hello.c -o a.s 我们将前面的示例编译成汇编代码。

    # 1 "hello.c" # 1 "<built-in>" # 1 "<command-line>" # 31 "<command-line>" # 1 "/usr/include/stdc-predef.h" 1 3 4 # 32 "<command-line>" 2 # 1 "hello.c" .file "hello.c" .text .section .rodata .LC0: .string "hello world!" .text .globl main .type main, @function main: .LFB5: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 leaq .LC0(%rip), %rdi movl $0, %eax

    汇编

    简单的讲就是将汇编语言转成机器语言。

    gcc -c hello.c -o a.o

    链接

    链接的过程就是将不同的二进制文件库绑定到一起。链接主要分为两种:静态链接与动态链接。

    静态链接

    静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一块生成可执行程序。这里的库指的是静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。

    优点:

    装载速度快,执行速度比动态链接快只需保证在开发者的计算机中有正确的.lib文件,在以二进制形式发布程序时不需考虑在用户的计算机上.lib文件是否存在及版本问题。

    缺点: 用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费。

    动态链接

    动态链接(Dynamic Linking),把链接这个过程推迟到了运行时再进行,在可执行文件装载时或运行时,由操作系统的装载程序加载库。这里的库指的是动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。值得一提的是,在Windows下的动态链接也可以用到.lib为后缀的文件,但这里的.lib文件叫做导入库,是由.dll文件生成的。

    优点:

    生成的可执行文件较静态链接生成的可执行文件小;适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试;不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性;

    缺点:

    使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息;速度比静态链接慢;

    在实际调试过程中,我们可以通过ldd或者nm文件来查看依赖关系。

    ldd -r postgis-2.5.so linux-vdso.so.1 (0x00007ffcd14df000) libgeos_c.so.1 => /usr/lib/x86_64-linux-gnu/libgeos_c.so.1 (0x00007f8ff7402000) libproj.so.12 => /usr/lib/x86_64-linux-gnu/libproj.so.12 (0x00007f8ff7199000) libjson-c.so.3 => /lib/x86_64-linux-gnu/libjson-c.so.3 (0x00007f8ff6f8e000) libxml2.so.2 => /usr/lib/x86_64-linux-gnu/libxml2.so.2 (0x00007f8ff6bcd000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f8ff69ae000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f8ff6610000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8ff621f000) libgeos-3.6.2.so => /usr/lib/x86_64-linux-gnu/libgeos-3.6.2.so (0x00007f8ff5e86000) libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f8ff5afd000) libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f8ff58e5000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f8ff56e1000) libicuuc.so.60 => /usr/lib/x86_64-linux-gnu/libicuuc.so.60 (0x00007f8ff5329000) libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f8ff510c000) liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 (0x00007f8ff4ee6000) /lib64/ld-linux-x86-64.so.2 (0x00007f8ff792f000) libicudata.so.60 => /usr/lib/x86_64-linux-gnu/libicudata.so.60 (0x00007f8ff333d000) undefined symbol: SPI_tuptable (./postgis-2.5.so) undefined symbol: CurrentMemoryContext (./postgis-2.5.so)

    如果存在一些未定以的符号也可以通过这个命令查看出来。对于函数符号的查询nm命令会更好用一些。

    动态链接库的命名

    在c/c++的动态链接由自己的命名规则都是lib+库名+.so+.版本号,比如libreadline.so.3;在链接过程中 链接器也会按照此规则来查找动态链接库;比如libreadline.so.3 我们只需要表示成-lbreadline

    链接库的路径遍历流程

    -l -L命令指定的文件夹LD_LIBRARY_PATH变量中的路径系统路径,比如 /usr/local/lib

    参考文献

    https://zhuanlan.zhihu.com/p/90890103

    Processed: 0.011, SQL: 9