C++ 全栈知识体系C++ 全栈知识体系
✿导航
  • 基础
  • 函数
  • 知识点
  • IO框架
  • 新版本特性
  • 数据库原理
  • SQL语言
  • SQL - MySQL
  • NoSQL - Redis
  • NoSQL - ElasticSearch
  • 算法基础
  • 常见算法
  • 领域算法
  • 分布式算法
  • 数据结构与算法
  • 计算机网络
  • 操作系统
  • 计算机组成
  • 开发
  • 测试
  • 架构基础
  • 分布式系统
  • 微服务
  • 中间件
  • 概念
  • 理论
  • 架构设计原则
  • 设计模式
  • 协议
  • 技术选型
  • 编码规范
  • 流水线构建 - CI/CD
  • 知识点 - Linux
  • 网站 - Nginx
  • 容器化 - Docker
  • 容器编排 - Kubernetes
  • 服务网格 - Service Mesh Istio
  • 常用快捷键 - Shortcut
  • 工具使用 - Tools
  • 开源项目
  • 学习项目
  • 个人项目
  • 项目开发
  • 项目Idea
  • 并发
  • 部署
  • 分布式
  • 知识
  • 问题
  • 编程语言与技术
  • 系统与架构
  • 软件开发实践
  • 数据处理与应用设计
  • 个人
  • 产品
  • 团队
  • 知识体系
  • Vue
关于
✿导航
  • 基础
  • 函数
  • 知识点
  • IO框架
  • 新版本特性
  • 数据库原理
  • SQL语言
  • SQL - MySQL
  • NoSQL - Redis
  • NoSQL - ElasticSearch
  • 算法基础
  • 常见算法
  • 领域算法
  • 分布式算法
  • 数据结构与算法
  • 计算机网络
  • 操作系统
  • 计算机组成
  • 开发
  • 测试
  • 架构基础
  • 分布式系统
  • 微服务
  • 中间件
  • 概念
  • 理论
  • 架构设计原则
  • 设计模式
  • 协议
  • 技术选型
  • 编码规范
  • 流水线构建 - CI/CD
  • 知识点 - Linux
  • 网站 - Nginx
  • 容器化 - Docker
  • 容器编排 - Kubernetes
  • 服务网格 - Service Mesh Istio
  • 常用快捷键 - Shortcut
  • 工具使用 - Tools
  • 开源项目
  • 学习项目
  • 个人项目
  • 项目开发
  • 项目Idea
  • 并发
  • 部署
  • 分布式
  • 知识
  • 问题
  • 编程语言与技术
  • 系统与架构
  • 软件开发实践
  • 数据处理与应用设计
  • 个人
  • 产品
  • 团队
  • 知识体系
  • Vue
关于
  • 编程语言与技术

    • Effective C++: 改善程序与设计的55个具体做法

      • 第2章 - 构造/析构/赋值运算(一)
      • 第2章 - 构造/析构/赋值运算(二)
      • 第2章 - 构造/析构/赋值运算(三)
      • 第3章 - 资源管理
      • 第4章 - 设计与声明(一)
      • 第4章 - 设计与声明(二)
      • 第5章 - 实现(一)
      • 第5章 - 实现(二)
      • 第6章 - 继承与面向对象设计
      • 第7章 - 模板与泛型编程
    • 深度探索C++对象模型

      • 第1章 - 关于对象
      • 第2章 - 构造函数语意学
      • 第3章 - Data 语意学
    • STL源码剖析

      • 第1章 - STL概论和版本简介
      • 第2章 - 空间配置器
      • 第3章 - 迭代器(iterators)概念与traits编程技法(一)
      • 第3章 - 迭代器(iterators)概念与traits编程技法(二)
      • 第4章 - 序列式容器 vector
      • 第4章 - 序列式容器 list
      • 第4章 - 序列式容器 deque
      • 第4章 - 序列式容器 stack和queue
      • 第4章 - 序列式容器 heap
      • 第4章 - 序列式容器 priority_queue
      • 第4章 - 序列式容器 slist
      • 第5章 - 关联式容器 RB-tree
      • 第5章 - 关联式容器 set和map
      • 第5章 - 关联式容器 hashtable
      • 第6章 - 算法
      • 第6章 - 算法之set
      • 第7章 - 仿函数
      • 第8章 - 配接器
  • 系统与架构

    • 深入理解计算机系统

      • 第1章 - 计算机系统漫游
      • 第2章 - 信息的表示和处理
      • 第3章 - 程序的机器级表示
      • 第5章 - 优化程序性能
      • 第6章 - 存储器层次结构
      • 第7章 - 链接
      • 第8章 - 异常控制流
      • 第9章 - 虚拟内存
      • 第10章 - 系统级I/O
      • 第11章 - 网络编程
      • 第12章 - 并发编程
    • 大型网站技术架构——核心原理与案例分析

      • 第1章 - 大型网站架构演化
      • 第2章 - 大型网站架构模式
      • 第3章 - 大型网站核心架构要素
      • 第4章 - 瞬时响应:网站的高性能架构
      • 第5章 - 万无一失:网站的高可用架构
      • 第6章 - 永无止境:网站的伸缩性架构
      • 第7章 - 随需应变:网站的可扩展架构
      • 第8章 - 固若金汤:网站的安全架构
    • 从零开始学架构

      • 架构基础
      • 架构设计原则
      • 高性能架构
      • 高可用架构
    • 程序员的自我修养————链接、装载与库

      • 第1章 - 简介
      • 第2章 - 静态链路
      • 第3章 - 目标文件里有什么
      • 第4章 - 静态链接
      • 第7章 - 动态链接
      • 第8章 - 共享库版本
      • 第10章 - 内存
      • 第11章 - 运行库
      • 第12章 - 系统调用与API
      • 第13章 - 运行库实现
  • 软件开发实践

    • 重构改善既有代码的设计

      • 第1章 - 重构,第一个示例
      • 第2章 - 重构的原则
      • 第3章 - 代码的坏味道
      • 第5章 - 重构列表
      • 第6章 - 重新组织函数
      • 第7章 - 在对象之间搬移特性
      • 第8章 - 重新组织数据
      • 第9章 - 简化条件表达式
      • 第10章 - 简化函数调用
      • 第11章 - 处理概括关系
      • 第12章 - 设计之大型重构
    • 代码大全2

      • 第1章 - 欢迎进入软件构建的世界
      • 第2章 - 用隐喻来更充分地理解软件开发
      • 第3章 - 三思而后行: 前期准备
      • 第4章 - 关键的构建决策
      • 第5章 - 软件构建中的设计
    • Linux多线程服务端编程——使用muduo C++ 网络库

      • Buffer类的设计
      • 设计与实现
      • 定时器与TimerQueue
      • Protobuf网络传输和Protobuf编解码器与消息分发器
      • EventLoop类剖析
      • EventLoopThread和EventLoopThreadPool剖析
      • TCP网络库和核心类
      • Connector剖析
      • TcpClient剖析
      • 学习总结
      • timing wheel
      • 消息广播服务
      • 线程安全的对象生命期管理
  • 数据处理与应用设计

    • 数据密集型应用系统设计

      • 第1章 - 可靠、可扩展与可维护的应用系统
      • 第2章 - 数据模型与查询语言
      • 第3章 - 数据存储与检索
      • 第4章 - 数据编码与演化
      • 第5章 - 数据复制
      • 第6章 - 数据分区
      • 第7章 - 事务

第3章 目标文件里有什么

  • 目标文件的格式
  • 目标文件是什么样的
  • 挖掘 SimpleSection.o
    • 代码段
    • 数据段和只读数据段
    • BSS段
    • 其他段
  • ELF文件结构描述
  • 链接的接口 ———— 符号
    • 符号修饰与函数签名
    • extern "C"

文章总结

概述

编译器编译源代码后生成的文件叫做目标文件。目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。

可执行文件格式涵盖了程序的编泽、链接、装载和执行的各个方面。了解它的结构并深入剖析它对于认识系统、了解背后的机理大有好处。

目标文件的格式

现在PC平台流行的可执行文件格式(Executable)主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format),它们都是COFF(Common flle format)格式的变种。目标文件就是源代码编译后但未进行链接的那些中间文件(Windows的.obj和Linux下的.o),它跟可执行文件的内容与结构很相似,所以一般跟可执行文件格式一起采用一种格式存储。

动态链接库(DLL,Dynamic Linking Library)(Windows的.dll和Linux的.so)及静态链接库(Static Linking Library)(Windows的.lib和Linux的.a)文件都按照可执行文件格式存储。ELF格式文件类型如下:

Linux下可以通过file命令查看相应的文件格式:

目标文件是什么样的

程序源代码编译后的机器指令经常被放在代码段(Code Section)里,代码段常见的名字有“.code”或“.text”:全局变量和局部静态变数据经常放在数据段(Data Section),数据段的一般名字都叫“.data”。看一个简单的程序被编译成目标文件后的结构:

C语言的编译后执行语句都编译成机器代码,保存在.text段:已经初始化的全局变量和局部静态变量都保存在.data段:未初始化的全局变量和局部静态变量一般放在一个叫 .bss 的段里。

.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。

程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和,bss段属于程序数据。

数据和指令分段好处:

  1. 权限清晰:分别映射到不同的虚拟缓存中,程序指令为只读,程序数据为可读写。
  2. 指令和数据分离有利于提供程序的局部性。
详情

程序指令和数据分离可以提供程序的局部性,从而改善程序的性能和效率。这是因为程序的局部性原理认为,程序倾向于在较短的时间内访问相邻的指令和数据,而不是随机地访问整个内存空间。

当程序的指令和数据被分离存储时,可以利用不同的存储机制来优化对它们的访问。常见的做法是将指令存储在一种高速缓存(如指令缓存)中,以提高指令的访问速度;而将数据存储在另一种高速缓存(如数据缓存)中,以提高数据的访问速度。

这种分离存储的方式有以下几个好处:

  1. 缓存命中率提高:由于程序的指令和数据具有局部性,将它们分开存储可以提高缓存命中率。当程序频繁访问某个指令或数据时,它们很可能已经被加载到缓存中,从而加速了对它们的访问。

  2. 缓存策略优化:指令和数据分离存储可以针对它们的访问的特点分别采用不同的缓存策略。指令通常具有较好的空间局部性,即相邻的指令有较高的概率被访问,因此可以采用预取(prefetching)策略来提前将指令加载到缓存中,从而减少指令访问的等待时间。而数据通常具有较好的时间局部性,即最近访问过的数据很可能在短时间内再次被访问,因此可以采用缓存替换策略(如LRU)来保留最近访问的数据,提高缓存的命中率。

  3. 提高内存带宽利用率:将指令和数据分离存储可以通过并行处理来提高内存带宽的利用率。由于指令和数据可以同时从不同的存储单元(如指令高速缓存和数据高速缓存)中读取,可以在一次内存访问中同时获取指令和数据,从而减少了读取数据的等待时间,提高了内存带宽的利用效率。

总而言之,程序指令和数据分离存储可以通过优化缓存命中率、缓存策略和内存带宽利用率来提供程序的局部性。这能够改善程序的性能和效率,加快指令和数据的访问速度,提高系统的整体运行效率。

  1. 当系统中运行着多个该程序的副本时,仅需保存一份指令代码。

挖掘 SimpleSection.o

以 SimpleSection.c 为例,分析目标文件内容

详情
int printf(const char *format, ...);

int global_init_var = 84;
int global_uinit_var;

void func1(int i)
{
    printf("%d\n", i);
}

int main()
{
    static int static_var = 85;
    static int static_var2;
    int a = 1;
    int b;
    func1(static_var + static_var2 + a + b);
    return a;
}

使用GCC来编译文件:

gcc -c SimpleSection.c

使用 objdump 来查看object内部的结构。

objdump -h SimpleSection.o 显示目标文件各个section的头部摘要信息。

只读数据段(.rodata)、注释信息段(.comment)和堆栈提示段(.note.GNU-stack)。

段的长度(Size)和段所在的位置(File Offset),每个段的第2行中的“CONTENTS”、“ALLOC”等表示段的各种属性,“CONTENTS”表示该段在文件中存在。BSS段没有“CONTENTS”,表示它实际上在ELF文件中不存在内容。“.note.GNU-stack”段虽然有“CONTENTS”,但它的长度为0,我们暂且忽略它,认为它在ELF文件中也不存在。

使用size命令,可以查看ELF文件的代码段、数据段和BSS段的长度。

[root@VM-16-6-centos test]# size SimpleSection.o
   text    data     bss     dec     hex filename
    95       8       4      107     6b  SimpleSection.o

代码段

objdump -s -d SimpleSection.o

objdump的“-s”参数可以将所有段的内容以十六进制的方式打印出来,“-d”参数可以将所有包含指令的段反汇编。我

详情
<!-- @include: ./src/objdump_SimpleSection -->

数据段和只读数据段

.data段保存的是那些己经初始化了的全局静态变量和局部静态变量。

“.rodata”段存放的是只读数据,一般是程序里面的只读变量(如const修饰的变量)和字符串常量。单独设立“.rodata”段有很多好处,不光是在语义上支持了C++的const关键字,而且操作系统在加载的时候可以将“.rodata”段的属性映射成只读,这样对于这个段的任何修改操作都会作为非法操作处理,保证了程序的安全性。

BSS段

.bss段存放的是未初始化的全局变量和局部静态变量,其实更准确的说法是.bss段为它们预留了空间。但是可以看到该段的大小只有4个字节,这与global-uninit-var和static-var2的大小的8个字节不符。实际上编译单元内部可见的静态变量的确是存在在.bss段的。

objdump -x -s -d SimpleSection.o

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  2 .bss          00000004  0000000000000000  0000000000000000  0000009c  2**2
                  ALLOC

tips

static int x1 = 0;
static int x2 = 1;

x1会被存放在.bss中,x2会被放在.data中。因为x1为0,可以认为是未初始化的,被优化放在.bss段,可以节省空间。

其他段

自定义段

GCC提供了一个扩展机制,使得程序员可以指定变量所处的段:

__attribute__((section("FOO"))) int global = 42;
__attribute__((section("BAR"))) void foo()
{
}

我们在全局变量或函数之前加上“attribute((section("name")))”属性就可以把相应的变量或函数放到以“name”作为段名的段中。

ELF文件结构描述

ELF目标文件格式的最前部是ELF文件头(ELF Header),它包含了描述整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址等。紧接着是ELF文件各个段。其中ELF文件中与段有关的重要结构就是段表(Section Header Table),该表描述了ELF文件包含的所有段的信息,比如每个段的段名、段的长度、在文件中的移、读写权限及段的其他属性。

readelf -h SimpleSection.o -h,--file-header:显示文件头信息

[root@VM-16-6-centos test]# readelf -h SimpleSection.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1056 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 12

从上面输出的结果可以看到,ELF的文件头中定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。

ELF文件头结构及相关常数被定义在“/usr/include/elf.h”里,因为ELF文件在各种平台下都通用,ELF文件有32位版本和64位版本。它的文件头结构也有这两种版本,分别叫做“Elf32_Ehdr”和“Elf、64_Ehdr。32位版本与64位版本的ELF文件的文件头内容是一样的,只不过有些成员的大小不一样。

以32版本的头文件结构“Elf32_Ehdr”为例

typedef struct{
    unsigned char e_ident[16];
    Elf32_Half e_type;
    Elf32_Half e_machine;
    Elf32_Word e_version;
    Elf32_Addr e_entry;
    Elf32_Off  e_phoff;//Start of program headers
    Elf32_Off  e_shoff;//Start of section headers
    Elf32_Word e_flags;
    Elf32_Half e_ehsize;
    Elf32_Half e_phentsize;
    Elf32_Half e_phnum;
    Elf32_Half e_shentsize;
    Elf32_Half e_shnum;
    Elf32_Half e_shstrndx;
}Elf32_Ehdr;

ELF魔数

最开始的4个字节是所有ELF文件都必须相同的标识码,分别为0x7F、0x45、0x4c、0x46,第一个字节对应ASCII字符里面的DEL控制符,后面3个字节刚好是ELF这3个字母的ASCII码。这4个字节又被称为ELF文件的魔数,几乎所有的可执行文件格式的最开始的几个字节都是魔数。接下来的一个字节是用来标识ELF的文件类的,0x01表示是32位的,0x02表示是64位的:第6个字是字节序,规定该ELF文件是人端的还是小端的(见附录:字节序)。第7个字节规定ELF文件的主版本号,一般是1,因为ELF标准自1.2版以后就再也没有更新了。后面的9个字节ELF标准没有定义,一般填0,有些平台会使用这9个字节作为扩展标志。

文件类型

机器类型

段表

前面使用了“objdump -h”来查看ELF文件中包含的段。可以使用read工具来查看ELF文件的段,它显示出来的结果才是真正的段表结构:

如图所示,段表就是有11个元素的数组。第一个元素时无效段,即共有10个有效段。

typedef struct
{
  Elf32_Word    sh_name;  /* Section name (string tbl index) */
  Elf32_Word    sh_type; /* Section type */
  Elf32_Word    sh_flags;/* Section flags */
  Elf32_Addr    sh_addr; /* Section virtual addr at execution */
  Elf32_Off     sh_offset; /* Section file offset */
  Elf32_Word    sh_size; /* Section size in bytes */
  Elf32_Word    sh_link; /* Link to another section */
  Elf32_Word    sh_info; /* Additional section information */
  Elf32_Word    sh_addralign;/* Section alignment */
  Elf32_Word    sh_entsize;  /* Entry size if section holds table */
} Elf32_Shdr;

SimpleSection的所有段的位置和长度给分析清楚了。如图所示:

重定位表

SimpleSection.o中有一个叫做“.rel.text”的段,它的类型(sh-type)“SHTREL”,即是一个重定位表(Relocation Table)。**链接器在处理目标文件时,须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。**这些重定位的信息都记录在ELF文件的重定位表里面,对于每个须要重定位的代码段或数据段,都会有一个相应的重定位表。

字符串表

常见的段名为“.strtab”或“.shstrtab”。这两个字符串表分别为字符串表(String Table)和段表字符串表(Section Header String Table)。字符串表用来保存普通的字符串,比如符号的名字:段表字符串表用来保存段表中用到的字符串,最常见的就是段名(sh-name)。

链接的接口 ———— 符号

链接过程的本质就是要把多个不同的目标文件之间相互“粘”到一起。在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。

可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。链接过程中很关键的一部分就是符号的管理,**每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里面记录了目标文件中所用到的所有符号。**每个定义的符号有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,符号值就是它们的地址。除了函数和变鼠之外,还存在其他几种不常用到的符号。

可以使用 nm 命令查询 “SimpleSection.o” 的符号结果:

符号修饰与函数签名

为了防止类似的符号名冲突,UNIX下的c语言就规定,c语言源代码文件中的所有全局的变量和函数经过编译以后,相对应的符号名前加上下划线“_”。C++增加了命名空间的方法来解决多模块的符号冲突问题。

C语言符号修饰

详情
int func(int x)
{
    return 0;
}

int main(int argc, char *argv[])
{
    int j = func(1);
    return 0;
}

gcc test.c -o testc
nm testc

00000000004004ed T func
00000000004004fb T main

g++ test.c -o testcpp
nm testcpp

000000000040051b T main
000000000040050d T _Z4funci

通过nm区分到底是C还是C++的编译结果

C++符号修饰

众所周知,强大而又复杂的C++拥有类、继承、虚机制、重载、名称空间等这些特性,它们使得符号管理更为复杂。最简单的例子,两个相同名字的函数func(int)和func(double),尽管函数名相同,但是参数列表不同,这是c++里面函数重载的最简单的一种情况。为了支持c++这些复杂的特性,人们发明了符号修饰(Name Decoration)或符号改编(Name Mangling)的机制。

int func(int);
float func(float);

class C {
  int func(int);
  class C2{
    int func(int);
  };
};

namespace N{
  int func(int);
  class C {
    int func(int);
  };
};

引入一个术语叫做函数签名(Function Signature), 函数签名包含了一个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间及其他信息。函数签名用于识别不同的函数。

GCC的基本c++名称修饰方法如下:所有的符号都以 “_Z” 开头,对于嵌套的名字(在名称空间或在类里面的),后面紧跟“N”,然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以 “E” 结尾。比如N::C::func经过名称修饰以后就是 _ZN1N1C4funcE。对于一个函数来说,它的参数列表紧跟在"E”后面,对于int类型来说,就是字母“i”。所以整个N::C::func(int)函数签名经过修饰为 _ZN1N1C4funcEi。

Visual C++ 编译器下,修饰后名称如图:

由于不同的编译器采用不同的名字修饰方法,必然会导致由不同编译器编译产生的目标文件无法正常相互链接,这是导致不同编译器之间不能互操作的主要原因之一。

extern "C"

c++为了与c兼容,在符号的管理上,c++有一个用来声明或定义一个C符号的 extern "C" 关键字用法:

extern "C"
    int func(int);
    int var;
}

c++编译器会将在extern "C"的大括号内部的代码当作C语言代码处理。所以很明显,上面的代码中,c++的名称修饰机制将不会起作用。

使用C++的宏“__cplusplus”判断当前编译单元是不是C++代码,实现平滑过渡

#ifdef __cplusplus
extern "C" {
#endif

void *memset(void *, int, size_t);

#ifdef __cplusplus
}
#endif

如果当前编译单元是C++代码,那么memset会在extern "C"里面被声明:如果是C代码,就直接声明。

Last Updated:
Contributors: klc407073648
Prev
第2章 - 静态链路
Next
第4章 - 静态链接