c笔记06--编译与作用域

编译流程与目标文件

编译器编译流程

编译器编译流程参考资料

编译四阶段: (注意各个阶段输入输出的文件后缀变化)

(图片来源)

  • .c,.cpp,.h – 源程序文件
  • .i,.ii --无宏,无特殊符号的文本文件
  • .s – 汇编文件
  • .o,.obj – 目标文件
  • .exe – 可执行文件

四个阶段总结:

  • step1(预处理阶段): 处理预编译指令,有宏文本换为无宏文本
  • step2(编译阶段): 无宏文本译为中间代码(汇编代码)
  • step3(汇编): 中间代码(汇编)译为二进制目标文件
  • step4(链接): 多个二进制目标文件合并为一个可执行文件

ELF文件

ELF(Executable and Linkable Format,可执行和可链接格式)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。
ELF文件分类:

  • 可重定位文件(Relocatable File) .o/.obj)包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。

  • 可执行文件(Executable File) .exe) 包含适合于执行的一个程序,此文件规定了exec() 如何创建一个程序的进程映像。

  • 共享目标文件(Shared Object File) .so) 包含可在两种上下文中链接的代码和数据。
    首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理, 生成另外一个目标文件。
    其次动态链接器(Dynamic Linker)可能将它与某 个可执行文件以及其它共享目标一起组合,创建进程映像。

因此,我们编译链接C源程序的step3和step4得到的文件(.o与.exe)都属于ELF文件.

可重定位文件(Relocatable File,即.o/.obj文件)

1.文件结构: (图片来源)

2.各主要部分简介:

  • ELF头: 目标文件的整体结构信息:文件类型(可重定位的,可执行的),数据表示方法(大小端表示),program header、section header等的起始地址、文件偏移大小、各段的节数目
  • .text: 源程序的二进制机器码形式.
  • .rodata: read only data,只读数据(如程序中出现的各种常量)
  • .data: 已初始化的数据(已经定义并赋值的符号)。注:局部C变量在运行时保存在栈中,既不出现在.data节中也不出现在.bss节中
  • .bss: Block Started by Symbol,未初始化和被初始化为零的数据.(只占用运行时空间,不占用ELF文件空间)
  • .symtab: symbol table,符号表,存放在程序中定义和引用的符号信息(函数和全局变量)

可执行文件(executable file,即.exe文件)

1.文件结构: (图片来源)

利用objdump命令查看可重定位文件的结构


程序在计算机中的运行

计算机运行程序的流程步骤

1.一般计算机的架构: (图片来源:爱课程)

2.调用程序时,系统创建程序进程,并将存储于硬盘上的可执行文件加载拷贝到主内存之中:(图片来源:爱课程)

3.系统根据文件中的信息,搭建程序运行的环境(分配空间,组织内存,初始化变量等等),并执行程序: (图片来源:爱课程)

4.辨析:

  • ELF文件中储存的只是信息和数据,是指导计算机搭建环境和运行程序的指南,程序的运行并不是发生在ELF文件中,而是计算机搭建的环境中.(打个比方,计算机好比一台多功能的机器,而ELF文件好比完成某一个功能的操作说明书.如果计算机是电视机,那么ELF就好比"如何换台"这一功能的说明)

程序的运行时内存模型

1.计算机的内存分配:
 计算机中的内存是分区来管理的,程序和程序之间的内存是独立的,不能互相访问,比如QQ和浏览器分别所占的内存区域是不能相互访问的。而每个程序的内存也是分区管理的,一个应用程序所占的内存可以分为很多个区域,我们需要了解的主要有四个区域,通常叫内存四区.

(图片来源)

(图片来源)

说明:

1.静态区包括 .text(代码段).rodata(常量段), .data(初始化非零的变量), .bss(未初始化/初始化为零的变量) 四个部分.
2.由上图可以看出,.text, .rodata, .data 三部分的内容分别是拷贝的可执行文件的对应部分内容
3.静态区内保存的值是C语言中用extern,static两个存储类型关键字修饰的变量以及一般的常量.C语言中共有四个存储类型修饰关键字,分别是auto,register,static和extern:

  • auto修饰的变量,是局部变量,程序运行时由系统于合适的时候在栈之中自动分配内存和释放内存.(C语言中,函数内部定义的变量与块结构内部的变量,也就是函数作用域与块作用域中的变量,默认为auto修饰)
  • register修饰的变量为寄存器变量,一般为函数形参的存储类型.
  • static修饰的变量为静态变量,初始化非零时存放在.data段,初始化为零时存放在.bss段.
  • extern修饰的变量为外部变量,只能在文件作用域中定义和初始化,在其他作用域只能引用.存放位置同static中的规则相同.
  • 注: 另外C语言中变量还有external linkage和internal linkage两种链接属性与static和extern两个关键字有关.但两者并不等同,被extern修饰的变量不一定属于external linkage,被static修饰的变量不一定属于internal linkage,要注意辨析与区分.

符号表

1.什么是符号表?

在计算机科学中,符号表是一种用于语言翻译器(例如编译器和解释器)中的数据结构。在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。
 符号表在编译程序工作的过程中需要不断收集、记录和使用源程序中一些语法符号的类型和特征等相关信息。这些信息一般以表格形式存储于系统中。如常数表、变量名表、数组名表、过程名表、标号表等等,统称为符号表。对于符号表组织、构造和管理方法的好坏会直接影响编译系统的运行效率。
 符号表的操作和进行操作的时机:


说明:
1.当定义性出现时,查找符号表是否有已经定义的与之冲突的同名符号,若无,新增符号表项目(填入名称和信息)
2.当使用性出现时,查找符号表是否有相关项目,若有,访问相关信息,将使用性符号替换为内存地址,将相关语句翻译为二进制机器码
3.当C语言某一个块中的语句或者函数调用执行完毕时,会退出块作用域/函数作用域到上一级作用域,栈内存执行出栈操作,同时符号表相关的项目执行删除操作.
4.当c语言中的变量被执行类型转换之类的操作时,对应的项目进行修改操作.

2.ELF文件中的符号表节区(.symtab)

  • 该节区中只保存了static和extern类型的符号信息,而register(函数形参)和auto类型的符号不出现在该节区之中.
  • 为什么ELF文件的符号表节区不包括auto局部变量呢?
      ELF文件是编译过程的输出,是最终的结果,它的符号表节区(.symtab)也仅仅是符号表操作的最终结果,无法反映整个编译过程的符号表变化.
  • 为什么ELF文件中需要保存static和extern的符号表而不需要保留auto局部变量呢?
      因为static和extern类型变量是在程序运行的整个过程都会保留的,因此保存在静态区.而这样的话,链接过程就需要重定位来保证虚拟地址和运行地址一致而不出错.而重定位需要用到static和extern的信息,故而ELF文件需要保留其符号表,
      而auto类型局部变量是使用时系统自动在栈中分配内存,退出作用域时又会出栈,释放内存.相应的地址是通过相对SP指针的偏移量来确定的,是相对地址,其翻译成的二进制机器码是与内存绝对位置无关的,不需要重定位,因而ELF文件u需要保留.

3.借助"符号表"的概念来理解C语言中的变量作用域问题

1.说明:

(1)我们这里所说的符号表指的是整个编译过程中的符号管理及其组织形式,不是ELF文件中的用于重定位的"符号表节区",这一点必须首先指出来.

  • ELF的符号表节区是将所有extern与static的变量统一记录存储起来,完全是同等看待的,没有什么因作用范围不同而带来的区别.
  • 而这些static或extern变量的作用范围,有效区域 等,都是在编译过程中经由我们这里所说的符号表的操作规则和组织原则来体现,并最终反映到二进制可执行文件的机器代码之中的.

(2)变量作用域,本质上是由符号表这种数据结构的实现决定的.变量作用域是符号表组织与管理的外在表现.因此,用符号表的概念来理解作用域的种种特征,从根源上把握,更能有所得.
(3)这里,我们只是借助符号表的概念和相应的操作来帮助我们更好地理解和记忆C变量作用域的特征,是假想的符号表系统,绝不是C符号表这种数据结构的具体实现.

我们假想的各种符号表:(后续链接属性部分会增加"库表")

  • 文件表:定义在文件内,所有函数/块/结构之外的符号所组成的表,在我们假想的符号表管理系统中属于文件的最外层表.
  • 局部表: 各个块作用域/函数作用域中的auto类型变量或数据的符号表.
    • 每进入一个块/函数,就会创建一个对应的假想局部表. ’
    • 每个局部表都有一个标识项Flag来指向自己在符号表系统中的父级表.

4.一般情况下,在假想符号表系统中查询某一符号名的规则:

定义性语句出现时:

  • 在本块/本函数的假想符号表中查找该符号.
    • 若不存在,创建符号项目,添加到当前局部表
    • 若已存在该符号,报错–“重复定义”

使用性语句出现时:

  • (1)先在本块/本函数的假想符号表中查找该符号
  • (2)如果不存在,到其父级表中继续查找;如果找到,访问信息,结束查找
  • (3)不停进行(2)的操作,查询的终点是文件表
  • (4)如果文件表中也没有,报错–“符号未定义”.

5.图示说明

代码:

#include <stdio.h>
long int a = 1; //定义性语句,文件域默认为extern类型,添加符号项目到文件表
static float b = 2.0F; //定义性语句,添加符号项目到到文件表
static char d = 'd'; //定义性语句,添加符号项目到到文件表

int main(){
    int a = 10; //定义性语句,添加符号项目到main函数的局部表1
    float b = 20.0F; //定义性语句,添加符号项目到main函数的局部表1
    static char c = 'c'; //定义性语句,添加符号项目到main函数的局部表1

    for(int a = 100;a < 101; a++){  //定义性语句,添加符号项目到for语句的局部表1-1;
        printf("这里的a的值为 : %d
",a); //使用性语句,在符号表系统中查找,在当前局部表1-1中存在,并访问其值100
    }

    fun(); //函数调用,创建函数fun的局部表1-2

    printf("这里的d的值为 : %c",d);  //使用性语句,在表格系统中查找符号d,在局部表1中不存在,在文件表中存在,并访问值'd'
    return 0;
}

void fun(){
    int a = 1000; //定义性语句,创建符号项目并添加到局部表1-2
    if(a>100){ //使用性语句,在文件系统中查找符号a,此时,它在if语句的局部表1-2-1中不存在,在局部表1-2中存在,访问其值1000
        int a = 1; //定义性语句,添加符号项目到局部表1-2-1
        printf("这里的b的值为 : %f
这里的a的值为 : %d
",b,a); //使用性语句,在符号表系统中查找该符号,在当前局部表1-2-1中存在,访问其值1
    }
}   

运行结果:

符号表组织形式:

6.关于命名空间

(1)关于命名空间,我们可以做出这样的假象模型来帮助理解:

  • 每一个符号表(文件表,局部表等)都按照符号的类别分为四页

  • 声明/定义任何一个符号,都必然属于这四个页之一(四页的并集是一个符号完备集)

  • 页与页之间相互独立(四页无交集,且相同符号名出现在不同页之中,不算重复定义)
    图示:

    说明:

    • lable页 : 同一符号表内的所有标签属于同一页
    • tag页 : 同一符号表内所有结构体名,枚举类型名,联合体名
    • member页(一类页) : 同一符号表内的同一个结构体/联合体内的各个成员之间同属于一个member页
    • other 页 : 除了上面三种,其他的符号属于同一页–other页

(2)因为任何符号定义都必然属于四页之一,查询某一符号时,仅在其对应所属的页中查询(整个符号表系统都仅在该页中查询该符号).

7.extern修饰符与变量声明,定义,初始化的关系

  • 声明(声明外部变量)
    严格来说,变量的声明是向编译器说明一个变量,这个行为是不分配内存空间的.
    例如:
    extern int ivalue;
    指明ivalue是别处的一个已经定义好的变量,在这里我只是使用一下,所以要向编译器先声明。声明也可以去掉类型名:extern ivalue;

  • 定义:
    在声明变量的同时也为变量分配了存储空间.定义的实质就是分配内存空间。
    例如:
    int ivalue;
    注意 : 定义是不能省略类型名的,因为要按类型决定分配的内存大小.

  • 初始化:
    定义变量/函数的同时为之赋值.
    例如:
    extern int ivalue = 10;

  • 总结:

    • 对于非extern类型的变量/函数,声明就是定义,本文件一律为之分配内存(static在静态区分配;auto类型在栈中分配)
    • 对于extern类型的变量/函数 : 不赋值就是声明,不分配内存;赋值就是初始化,分配内存:
      • extern int ivalue; 属于声明,引用别处定义的符号,不分配内存
      • extern int ivalue = 10; 属于初始化,自定义的符号,要分配内存
      • extern int ivalue;ivlaue = 10;属于声明,引用别处定义的符号,然后修改其中的值,本文件(main.c)中不为之分配内存
    • 特别注意:
      • extern类型变量/函数的初始化,只在文件作用域是合法的,在局部作用域(块作用域,函数作用域)是非法的
      • 在局部作用域,只能声明extern变量(也就是引入外部符号),不能定义和初始化extern变量.

8.关于链接属性

(1)链接过程是什么?
总结:

  • 一个源文件(.c/.cpp)就是一个模块或编译单元,经过编译后生成.o/.obj文件
  • 一个完整的程序必须依赖多个模块,即多个.o/.obj文件
  • 链接过程就是按照一定的约定,将用到的各个编译单元生成的.o/.obj文件组合到一起,生成可执行文件(.exe)

(2)链接过程对于符号表系统的影响,我们可以等价为如下模型:

说明:假设整个程序的运行需要依赖三个模块main.c(文件1),mod1.c(文件2)和mod2.c(文件3)
  链接过程就是将三个文件各自的文件作用域内定义/初始化的extern变量或函数拿出来,新组成一个库表(extern linkage表)(所有文件共享一个extern linkage表)
注意 : 库表与文件表的辨析

  • 库表中的符号,需要用extern声明才可以查找和访问
  • 库表和各个文件表有重合部分(文件表本身定义并初始化的extern类型变量,既属于文件表本身,也属于库表),所以,对于重合部分,既可用extern引用访问,也可根据文件自身一般情况下的访问规则访问.

9.引入链接属性后,完整的符号表查表操作规则为:

(1)定义external linkage,internal linkage变量相关问题测试

  • 定义external linkage和internal linkage只在文件作用域才是合法和有效的

    • 对于external linkage
      • 定义/初始化 external linkage符号,只能在文件域中进行(局部作用域中会报错)
      • 文件作用域,局部作用域都可以用extern引用外部符号,并在之后修改其值
    • 对于internal linkage
      • 只有文件域中用static修饰的符号才具有internal linkage,局部静态变量不具备internal linkage
      • 假设文件中符号C具有internal linkage,它的类型为type.
        那么语句extern type C;将不再引入库表中的同名符号,而一律引用这个具有internal linkage的符号C(这一特点我们用符号表模型简化的结果就是: extern 声明的符号在使用时,先在文件表中查找,无结果再在库表中查找)
  • 其他问题的测试


(2)引入链接属性后的查询规则可以归纳为如下的图形:


说明:
1.当遇到extern符号声明(引入符号)时,直接跳到文件表中查找该符号.

  • 如果查找到(当前文件作用域中定义/初始化的具有internal/external的变量),访问该符号信息.
  • 如果文件表钟无结果,跳到库表中查找该符号.如果查询到结果(其他文件定义的external linkage符号),访问该符号信息;如果仍然没有查询到,报错"符号未定义".

举例说明:

分析:
第12行–声明符号a
第13行–申明符号b
第14行–符号a使用性出现,进行符号表的查找操作

  • 第一步:到文件表中查找符号a
    文件表(main.c)中符号有 : internal linkage符号 a , external linkage符号 b.
    查到结果,取得a的值为1,打印到屏幕,结束.(注意,extern声明的符号使用时,先查文件表,文件表没有再查库表.本例中,在文件表中已经得到了结果,因此虽然库表中也有一个符号a=20,但是不会用到)

第15行–符号b使用性出现,进行符号表查找操作

  • 第一步:到文件表中查找符号b : 文件表中无结果
  • 第二部:到库表中查找符号b : 库表中有 external linkage符号b,取得b的值为20,打印,结束.
原文地址:https://www.cnblogs.com/peterzhangsnail/p/12521652.html