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\n", 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", 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 << '\n'
 
int main()
{
    std::cout << "abcd: " << fun_abcd() << '\n';
    std::cout << "fff: " << fun_fff() << '\n';
    std::cout << "qqq: " << fun_qqq() << '\n';
    std::cout << FUNCTION << '\n';
    OUTPUT(million);               // 注意这里没有引号
}

  

输出:

abcd: 12
fff: 2
qqq: 23
34
million

  

Done!!!

引用:
http://0xffffff.org/2013/04/17/17-complier-and-linker-2/

posted on 2017-08-09 22:59  chaunceyctx  阅读(158)  评论(0编辑  收藏  举报

导航