• 总结(二)


    编译和链接

    源程序变成可执行程序的过程

    源程序 hello.c
    预编译:

    1. 将所有#define展开
    2. 处理#if #endif 等宏
    3. 删除注释

    gcc -E hello.c -o hello.i
    将源程序编译成hello.i
    编译:(编译原理内容)
    gcc -S hello.i -o hello.s
    经历词法分析、语法分析、语义分析及优化产生汇编代码
    汇编:
    汇编器将汇编代码转变成机器码
    gcc -c hello.s -o hello.o
    链接
    gcc -static --verbose hello.c

    /usr/libexec/gcc/x86_64-redhat-linux/4.8.5/collect2 --build-id --no-add-needed --hash-style=gnu -m elf_x86_64 -static /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crti.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbeginT.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../.. /tmp/ccbaCOfD.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crtn.o
    

    删除路径后就是这样

    collect2 --build-id --no-add-needed --hash-style=gnu -m elf_x86_64 -static crt1.o crti.o crtbeginT.o ccbaCOfD.o(hello.o) --start-group -lgcc -lgcc_eh -lc --end-group crtend.o crtn.o
    

    连接过程主要包括地址和空间的分配符号决议重定位等步骤。

    目标文件的格式

    可执行文件文件格式:
    Windows的PE(Portable Executable)和Linux下的ELF(Executable Linkable Format)

    ELF文件

    包含可重定位文件,可执行文件,共享目标文件,核心转储文件
    Linux可以使用file命令查看文件类型信息

    ELF文件的内容

    首先,包含编译后的机器指令代码、数据。其次包含链接时需要的一些信息。
    目标文件的信息通常以Section来存储,在链接的角度上,Section是将相同的类型的信息聚合在一块儿。比如说代码段(Code Section),数据段(Data Section)
    为了减少内存的损耗和浪费目标文件的信息,从装载的角度上来看,目标文件的信息通常使用Segment来及进行储存,Segment是以可执行文件信息的权限进行分配,rwx这样的权限。(后面章节的内容)
    目标文件的例子:

    /**
    *	SimpleSection.c
    */
    int printf(const char* format, ...);
    
    int global_init_var = 84;
    int global_uninit_var;
    
    void func1(int i) {
    	printf("%d
    ", i);
    }
    
    int main() {
    	static int static_var = 1;
    	static int static_var2;
    
    	int a = 1;
    	int b;
    	
    	func1(static_var + static_var2 + a + b);
    
    	return a;
    }
    

    使用编译选项编译成SimpleSection.o gcc -c SimpleSection.c
    使用objdump工具-h选项来查看elf文件查看基本信息

    其中包含的几个Section有.data数据段 .text代码段 .bss未初始化的全局变量和局部静态变量 .rodata只读数据段 .comment注释段 .note.GUN-stack堆栈提示段
    上面的表头的意思分别是,该段所在的段表的下表,该段名称,段大小,虚拟地址(Virtual Memory Address),装载地址(Load Memory Address), 文件偏移大小, 对齐大小。
    其中每个段(Section)的第二行对应着每个段(Section)属性。

    代码段
    其中存放着程序运行的指令信息,可以通过objdump -d 反汇编查看,机器指令信息对应的汇编语言代码
    数据段和只读数据段

    数据段存放着两个数,看了一下自己的电脑是小端模式,就是低位存放在低地址中,所以两个数应该是 0x00000054 = 84 0x00000001 = 1,正是全局变量和静态变量的值
    只读数据段中只放了字符串常量 "%d " 对应着 25 64 0a 00
    BSS段
    存放着未初始化的全局变量和局部静态变量,对于未初始化的全局静态变量有些会放在bss段中,有些则当作符号来处理,只保留一个未定义的全局变量的符号。

    Tip: static int x1 = 0; 和 static int x2 = 1;许多编译器将x1当作为未初始化的变量存储的bss中

    文件结构

    文件头

    文件头包含着描述整个文件的基本属性:
    ELF版本,目标机器型号,程序入口地址等,还有其中最重要的段表
    查看ELF文件头的内容readelf -h SimpleSection.o

    其中关于ELF头文件结构的结构体定义在/usr/include/elf.h中

    参照课本的解释:

    1. Magic前16个字节 -- e_ident[EI_NIENT]
      前四个字节 0x7f(DEL) 45(E) 4c(L) 46(F) 表示是ELF
      第五个字节 00 表示无效文件 | 01 表示32位 | 02表示64位
      第六个字节 00 表示无效 | 01 表示小端 | 02 表示大端
      第七个字节 表示ELF的主版本号
      后面的9个字节是扩展字节,没有定义,填0
    2. e_type ELF文件类型,此处为可重定位文件(REL) 还有ET_EXEC 和 ET_DYN
    3. e_machine 机器的CPU平台属性
    4. e_version ELF版本号, 一般1
    5. e_entry 程序入口地址
    6. e_phoff
    7. e_shoff 段表在文件中的偏移
    8. e_flags 标志位,用来标志平台相关属性
    9. e_ehsize 文件头大小
    10. e_phentsize
    11. e_phnum
    12. e_shentsize段表描述符大小,表头大小
    13. e_shnum段表表头的数量
    14. e_shstrndx段表的字符串表在段表中的位置下表

    段表

    查看段表内容:

    头文件/usr/include/elf.h中的段表的结构

    1. sh_name段名
    2. sh_type段类型
    3. sh_flags段的标志位
    4. sh_addr段的虚拟地址
    5. sh_offset段在文件中的偏移
    6. sh_size段的长度
    7. sh_link段的链接信息
    8. sh_info
    9. sh_addralign对齐方式
    10. sh_entsize项的长度,0表示没有固定长度
      这里有几个表:
      段类型:

    段标志:

    系统保留段的属性一览:

    段的链接信息:(后面章节)

    符号--链接的接口

    SimpleSection.o的符号

    符号表的结构也储存在elf.h的头文件里面

    符号绑定信息

    • 0 STB_LOCAL 局部符号外部不可见
    • 1 STB_GLOBAL 全局符号外部可见
    • 2 STB_WEAK 弱符号
      符号类型
      NOTYPE 未知类型
      FILE 文件名
      SECTION 段
      OBJECT 数据对象
      FUNC 函数或可执行代码
      符号所在段
      ABS 表示绝对值,比如文件名
      COM 比如未初始化的全局变量,COMMON块
      UNDEF 未定义
      特殊符号
      在链接过程中产生的符号,比如ld链接器产生的符号end edata等

    C与C++的符号修饰和函数签名

    C++支持函数重载,其理由是因为在编译过程中处理了符号冲突的发生,其解决办法是对符号进行修饰,按照一定规则对函数名进行改写,包含信息有所述命名空间所属类其参数类型与个数等等
    例如在gcc下 int func(int) 改写后的名称为 _Z4funci,各个编译器的修饰方式不同。
    使用宏条件判断来处理C与C++的许多兼容性问题。

    强符号和弱符号

    当多个文件中包含相同名字的全局符号定义时,那么在目标文件链接的时候会出现符号重复定义的错误。
    对于C/C++语言来说,初始化的全局变量和函数是强符号,未初始化的全局变量时弱符号。
    对于强弱符号,链接器会应用一下原则:

    • 不允许强符号多次定义;如果有强符号的多次定义,链接器会报错。
    • 如果一个符号在目标文件中的强符号,其他文件中都是弱符号,那么链接器使用强符号定义。
    • 如果所有文件中的符号都是弱符号,那么链接器选择使用空间最大的那一个。

    Tips: 尽量不要使用不同类型的弱符号定义。
    弱符号的例子

    #include <stdio.h>
    #include <pthread.h>
    
    int pthread_create(
    		pthread_t*,
    		const pthread_attr_t*,
    		void *(*)(void *),
    		void*) __attribute__ ((weak));
    
    int main() {
    	if (pthread_create) {
    		printf("multi-thread version!
    ");
    	} else {
    		printf("single-thread version!
    ");
    	}
    	return 0;
    }
    

    对于声明的pthread_create的弱符号类型, 如果外部的pthread_create强符号被定义(链接到lpthread库),那么就是用外部的定义,否则使用本地定义的弱符号。所以可以通过这种方式来解决这类问题。
    弱引用
    这个是一个坑,书中的例子并没有跑起来。
    通过声明弱引用,可以让没有定义的函数符号在链接时并不会报错,如果外部有定义,那么就会连接到外部定义,功能可以正常使用,如果没有定义,程序也不会出错,实现可以扩展的功能。

    书中的一段描述:

    这种弱符号和弱引用对于库来说十分有用,库中的弱符号可以被用户的强符号所覆盖,从而使程序可以使用自定义的库函数;或者程序的扩展模块使用弱引用,将扩展模块和程序连接时,模块可以正常使用,如果去掉功能模块,程序也可以正常链接。

  • 相关阅读:
    NanUI文档
    NanUI文档
    NanUI文档
    开源组件NanUI一周年
    NanUI文档
    NanUI 0.4.4发布
    【开源】做了一个WinForm窗体的投影组件,能够为窗口添加影子效果
    NanUI for Winform 使用示例【第二集】——做一个所见即所得的Markdown编辑器
    NanUI for Winform 使用示例【第一集】——山寨个代码编辑器
    非常简洁简单的tcp socket库 XKSocket
  • 原文地址:https://www.cnblogs.com/Alruddy/p/9148049.html
Copyright © 2020-2023  润新知