• SunnyOS准备3


    gcc编译链接分解:

    主要过程为:

    预处理(preprocess)  ----->   编译(compilation) --------> 汇编(assembly)  ------>  链接(linking)

    gcc指令:

    -E:表示只执行预处理(preprocess)

    -S:执行预处理和编译

    -o:表示输出文件

    -c:表示预处理,编译和汇编操作

    -g:输出文件可让gdb调试

    例子(test.c):

    #include <stdio.h>
    #define print(a) printf(#a)
    int main(int argc,char argv*[]){
      print(hello world);
      printf("hello world"); //printf function
      return 0;
    }

    预处理:

    然后生成的test.i内容:

    一大堆的的变量定义和函数声明以及放在最后的我们编写的代码(在vim中使用shift+g跳转到最后)

    实际上前面的一大段代码就是stdio.h头文件内容,预处理会将我们包含的头文件以递归操作将其展开,并且预处理会删除我们所有注释

    预处理主要范围:

    • 展开所有的宏定义并删除 #define   (见例子)
    • 处理所有的条件编译指令,例如 #if #else #endif #ifndef …,这就帮助我们观察宏定义是否正确
    • 把所有的 #include 替换为头文件实际内容,递归进行(见例子)
    • 把所有的注释 // 和 / / 替换为空格(见例子)
    • 添加行号和文件名标识以供编译器使用
    • 保留所有的 #pragma 指令,因为编译器要使用

    gcc其实并不要求函数一定要在被调用之前定义或者声明(MSVC不允许),因为gcc在处理到某个未知类型的函数时,会为其创建一个隐式声明,并假设该函数返回值类型为int。但gcc此时无法检查传递给该函数的实参类型和个数是否正确,不利于编译器为我们排除错误(而且如果该函数的返回值不是int的话也会出错)。

    编译:

    编译就是把预处理之后的文件进行一系列词法分析、语法分析、语义分析以及优化后生成的相应汇编代码文件

    (在命令行加上参数 -masm=intel ,这样gcc就会生成Intel风格的汇编代码了)

    生成AT&T语法的test.s汇编文件

     汇编:

    把编译生成的汇编代码生成机器码

     

    test.o(很恐怖)

    不同的操作系统之间的可执行文件的格式通常是不一样的,所以造成了编译好的HelloWorld没有办法直接复制执行,而需要在相关平台上重新编译,当然了,不能运行的原因自然不是这一点点,不同的操作系统接口(windows API和Linux的System Call)以及相关的类库不同也是原因之一。

    链接:

    链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行。

    链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器(loader)加载到存储器并执行时;甚至执行于运行时(run time),由应用程序来执行

    任何一个程序,它的背后都有一套庞大的代码在支撑着它,以使得该程序能够正常运行。这套代码至少包括入口函数、以及其所依赖的函数构成的函数集合。当然,它还包含了各种标准库函数的实现这个“支撑模块”就叫做运行时库(Runtime Library)。而C语言的运行库,即被称为C运行时库(CRT)。

    CRT大致包括:启动与退出相关的代码(包括入口函数及入口函数所依赖的其他函数)、标准库函数(ANSI C标准规定的函数实现)、I/O相关、堆的封装实现、语言特殊功能的实现以及调试相关。其中标准库函数的实现占据了主要地位。例如printf,scanf函数就是标准库函数的成员。C语言标准库在不同的平台上实现了不同的版本,我们只要依赖其接口定义,就能保证程序在不同平台上的一致行为。

    C语言提供了标准库函数供我们使用,那么以什么形式提供呢?

    我们几乎每一次写程序都难免去使用库函数,每一次去编译标准库源代码太麻烦了。于是就事先把标准库函数提前编译好,需要的时候直接链接。

    标准库以什么形式存在呢?一个目标文件?

    我们知道,链接的最小单位就是一个个目标文件,如果我们只用到一个printf函数,就需要和整个库链接的话岂不是太浪费资源了么?但是,如果把库函数分别定义在彼此独立的代码文件里,这样编译出来的可是一大堆目标文件,有点混乱。

    1.静态链接库

    编辑器系统提供了一种机制,将所有的编译出来(按照库函数编译)的目标文件打包成一个单独的文件,叫做静态库(static library)。当链接器和静态库链接的时候,链接器会从这个打包的文件中“解压缩”出需要的部分目标文件进行链接,这就减少了资源浪费

    Linux/Unix系统下ANSI C的库名叫做libc.a,另外数学函数单独在libm.a库里。静态库采用一种称为存档(archive)的特殊文件格式来保存。其实就是一个目标文件的集合,文件头描述了每个成员目标文件的位置和大小。

    自制静态库:

    // swap.c
    void swap(int *num1, int *num2)
    {
        int tmp = *num1;
        *num1 = *num2;
        *num2 = tmp;
    }
    // add.c
    int add(int a, int b)
    {
        return a + b;
    }
    // calc.h
    #ifndef CALC_H_
    #define CALC_H_
    #ifdef _cplusplus
    extern "C"   
    {
    #endif
    void swap(int *, int *);
    int add(int, int);
    #ifdef _cplusplus
    }
    #endif
    #endif // CALC_H_

    C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。extern "C"指令仅指定编译和连接规约但不影响语义。例如在函数声明中,指定了extern "C",仍然要遵守C++的类型检测、参数转换规则。

    我们分别编译它们得到了swap.o和add.o这两个目标文件,最后使用ar命令将其打包为一个静态库。

    使用swap和add函数:

    //calc.c
    #include <stdio.h> #include <stdlib.h> #include "calc.h" int main(int argc, char *argv[]) { int a = 1, b = 2; swap(&a, &b); printf("%d %d ", a, b); return EXIT_SUCCESS; }

     步骤为:

    ps.这里将swap.o和add.o目标文件打包成libcalc.a静态库

    最终运行为:

    我们使用C语言标准库的时候,编译并不需要加什么库名,那是因为标准库已经是标准了,所以会被默认链接。

    缺点:

    每一个使用了相同的C标准函数的程序都需要和相关目标文件进行链接,浪费磁盘空间;当一个程序有多个副本执行时,相同的库代码部分被载入内存,浪费内存;当库代码更新之后,使用这些库的函数必须全部重新编译生成目标文件……

    2.为此引入动态链接库:

    动态链接库/共享库是一个目标模块,在运行时可以加载到任意的存储器地址,并和一个正在运行的程序链接起来。这个过程就是动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序完成的。Unix/Linux中共享库的后缀名通常是.so,window为dll文件。

    建立一个动态链接库:

    把上面ar命令改成gcc swap.c add.c -shared -o libcalc.so 即可

    之后执行gcc test.c -o test ./libcalc.so

    用ldd(ldd是我们在上篇中推荐的GNU binutils工具包的组成之一)分析动态链接文件的依赖

    在gcc编译的命令行加上 -static 可以要求静态链接

    好处:

    ①库更新之后,只需要替换掉动态库文件(swap.c,add.c)即可,无需编译所有依赖库的可执行文件。

    ②程序有多个副本执行时,内存中只需要一份库代码(通过动态链接器链接即可),节省空间。

    链接的步骤大致包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等主要步骤。

    什么是符号(symbol)?简单说我们在代码中定义的函数和变量可以统称为符号。符号名(symbol name)就是函数名和变量名

    例如:我们调用了printf函数,编译时留下了要填入的函数地址,那么printf函数的实际地址在那呢?这个空位什么时候修正呢?当然是链接的时候,重定位那一步就是做这个的(重定位是将符号引用与符号定义进行连接的过程)。

    符号决议:正如前文所说,编译期间留下了很多需要重新定位的符号,所以目标文件中会有一块区域专门保存符号表。那链接器如何知道具体位置呢?其实链接器不知道,所以链接器会搜索全部的待链接的目标文件,寻找这个符号的位置,然后修正每一个符号的地址

    符号查找问题:

    1.找不到符号:

    当我们声明了一个swap函数却没有定义它的时候,我们调用这个函数的代码可以通过编译,但是在链接期间却会遇到错误。形如“test.c:(.text+0x29): undefined reference to ‘swap’”这样,特别的,MSVC编译器报错是找不到符号_swap。

    为什么多了MSVC编译器会多个_?

    当C语言刚面世的时候,已经存在不少用汇编语言写好的库了,因为链接器的符号唯一规则,假如该库中存在main函数,我们就不能在C代码中出现main函数了,因为会遭遇符号重定义错误,倘若放弃这些库又是一大损失。所以当时的编译器会对代码中的符号进行修饰(name decoration),C语言的代码会在符号前加下划线,这样各个目标文件就不会同名了,就解决了符号冲突的问题,随着时间的流逝,操作系统和编译器都被重写了好多遍了,当前的这个问题已经可以无视了,而MSVC依旧保留了这个传统,所以我们可以看到_swap这样的修饰。

    存在同名符号时链接器如何处理?不是刚刚说了会报告重名错误吗?这不仅仅这么简单,在编译时,编译器会向汇编器输出每个全局符号,分为强(strong)符号弱(weak)符号,汇编器把这个信息隐含的编码在可重定位目标文件的符号表里。其中函数和已初始化过的全局变量是强符号,未初始化的全局变量是弱符号。根据强弱符号的定义,GNU链接器采用的规则如下:

    1. 不允许多个强符号
    2. 如果有一个强符号和一个或多个弱符号,则选择强符号
    3. 如果有多个弱符号,则随机选择一个

    具体例子:

    // link1.c
    #include <stdio.h>
    int n;
    int main(int argc, char *argv[])
    {
        printf("It is %d
    ", n);
        return 0;
    }
    
    // link2.c
    int n = 5;

    输出为5

    同理:

    main.c
    
    int main(int argc, char*argv[])
    {
        hello();
    }
    
    
    hello.c
    
    #include <stdio.h>  //甚至不 include hello.h
    void hello() //定义了为强符号,链接时符号选择该处
    {
      puts("hello world");      
    }
    
    
    hello.h
    void hello();//只申明弱符号

    编译链接,结果hello world

      

    这也就是为什么上面编写静态链接库的时候,void swap(*,*)和定义的swap会结合成一起,定义过的swap为强符号,申明的是弱符号,根据“如果有一个强符号和一个或多个弱符号,则选择强符号”,所以swap就是已经定义的那个

    对于宏定义:

    #是将单个宏参数转换成一个字符串
    ##则是将两个宏参数连接在一起

    在C宏中称为Variadic Macro,也就是变参宏。比如:
    #define myprintf(templt,...) fprintf(stderr,templt,__VA_ARGS__)

    // 或者

    #define myprintf(templt,args...) fprintf(stderr,templt,args)

    第一个宏中由于没有对变参起名,我们用默认的宏__VA_ARGS__来替代它。第二个宏中,我们显式地命名变参为args,那么我们在宏定义中就可以用args来代指变参了,变参必须作为参数表的最后一项出现。

    使用时C标准要求我们必须写成:
    myprintf(templt,);

    的形式
    例如:
    myprintf("Error!/n",);

    替换为:

    fprintf(stderr,"Error!/n",);/因为最后逗号出错

    C++支持:
    myprintf("Error!/n");//没最后逗号

    替换为:

    fprintf(stderr,"Error!/n",);因为最后逗号出错

    当使用##
    #define myprintf(templt, ...) fprintf(stderr,templt, ##__VAR_ARGS__)

    这时,##这个连接符号充当的作用就是当__VAR_ARGS__为空的时候,消除前面的那个逗号。那么此时的翻译过程如下:
    myprintf(templt);

    被转化为:

    fprintf(stderr,templt);

    #if MY_PRINTF_VERSION == 1
     void printf1() {
        ...
    }
    #elif MY_PRINTF_VERSION == 2
     int printf2()
    {
        ...
    }
    #elsif
      int printf3()
    {
        ...
    }
    #endif

      

    预编译时,当if符合时,编译printf1,逻辑与if ,else if ,else一样。

    #include <iostream>
     
    //make function factory and use it
    #define FUNCTION(name, a) int fun_##name() { return a;}
     
    FUNCTION(abcd, 12)
    FUNCTION(fff, 2)
    FUNCTION(qqq, 23)
     
    #undef FUNCTION
    #define FUNCTION 34
    #define OUTPUT(a) std::cout << #a << '
    '
     
    int main()
    {
        std::cout << "abcd: " << fun_abcd() << '
    ';
        std::cout << "fff: " << fun_fff() << '
    ';
        std::cout << "qqq: " << fun_qqq() << '
    ';
        std::cout << FUNCTION << '
    ';
        OUTPUT(million);               // 注意这里没有引号
    }
    

      

    输出:

    abcd: 12
    fff: 2
    qqq: 23
    34
    million
    

      

    Done!!!

    引用:
    http://0xffffff.org/2013/04/17/17-complier-and-linker-2/
  • 相关阅读:
    HDU 2955 Robberies(01背包)
    HDU 2602 Bone Collector(01背包)
    HUST 1352 Repetitions of Substrings(字符串)
    HUST 1358 Uiwurerirexb jeqvad(模拟解密)
    HUST 1404 Hamming Distance(字符串)
    HDU 4520 小Q系列故事――最佳裁判(STL)
    HDU 2058 The sum problem(枚举)
    【破解】修改程序版权、添加弹窗
    HDU 1407 测试你是否和LTC水平一样高(枚举)
    HDU 1050 Moving Tables(贪心)
  • 原文地址:https://www.cnblogs.com/chaunceyctx/p/7333747.html
Copyright © 2020-2023  润新知