• 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/
  • 相关阅读:
    app后端session共享问题
    nignx
    dubbo
    lucene&solr-day1
    SSM框架整合,以CRM为例子
    SpringMVC入门第二天
    HBase集群搭建
    SecureCRT的Home+End+Del键映射
    记一次让人的喷血的排错经历
    基于docker搭建mysql集群
  • 原文地址:https://www.cnblogs.com/chaunceyctx/p/7333747.html
Copyright © 2020-2023  润新知