操作系统系列(三)——编译和链接

2023-11-01

往期地址:

本期主题:

  • 编译和链接

1.被隐藏了的过程

在平常的应用程序开发中,我们很少关注编译和链接过程,这是因为通常的开发环境(IDE)都已经将这些给集成好了,我们只需要build即可,通常这种将编译和链接合并在一起的过程称为构建(build)
但是在这样的开发过程中,我们往往会被这些复杂的集成工具所提供的强大功能所迷惑,很多软件的运行机制和原理我们不是很理解,这样在遇到问题时,经常束手无策。
实际上一个简单的hello.c,在linux下使用gcc编译时,也可以分为4个步骤:

在这里插入图片描述
更为详细的过程如下图所示:
在这里插入图片描述

1.1 预编译

首先是源代码hello.c和相关的头文件,如stdio.h被预编译器cpp预编译成一个.i文件

$ gcc -E hello.c -o hello.i

  • 预编译过程目的:处理源代码中的以 “#” 开始的预编译指令,如"#include"、"#define"等
  • 预编译处理规则
    • 将所有#define删除,展开宏定义
    • 处理所有的条件预编译指令,比如 #if、#ifdef、#elif等
    • 处理#include预编译指令,将被包含的文件插入到预编译指令的位置
  • 预编译结果:经过预编译后生成的.i文件不含任何的宏定义,并且包含的文件也被插入到.i文件中,所以当我们无法判断宏展开的结果以及头文件包含是否正确时,可查看预编译后的文件来定位问题

1.2 编译

把预处理完的文件进行一系列词法分析、语法分析、语义分析后生成相应的汇编代码文件。

gcc -S hello.i -o hello.s

  • 编译目的:将预编译后的文件进行一系列语法分析后产生相应的汇编代码文件。
  • 所使用的程序:和预编译一样使用cc1

1.3 汇编

汇编器实际上是将汇编代码文件转变成可以被机器所执行的指令。

gcc -c hello.s -o hello.o

生成的.o文件是目标文件(object file),这一步生成的文件并不是一个可执行文件,需要将很多东西链接起来,才是可执行文件
文件属性:

jason@ubuntu:~/WorkSpace/0.Unix_AP/compile$ gcc -c hello.c -o test2
jason@ubuntu:~/WorkSpace/0.Unix_AP/compile$ file test2 
test2: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

1.4 链接

带着问题阅读本节,为什么在汇编这一步不直接生成一个可执行文件呢,而要再多这一步链接?

1.模块拼接——静态链接

程序设计的模块化一直是程序员所追求的目标,人们将每个模块的代码独立编译,然后按照某种规则将他们给组装起来,这个组装的过程就是链接

链接的过程主要包括:

  • 地址和空间分配
  • 符号决议
  • 重定位

在这里插入图片描述
基本的静态链接如上图所示:每个模块的源文件经过编译器编译生成目标文件(.o),目标文件和库一起链接形成最终可执行文件。
现在的链接器空间分配策略一般都采用两步链接的方式,整个链接过程被分为两步,分别是:

  1. 空间地址与分配
  2. 符号解析和定位

下面来详细说说。

2.空间地址与分配

过程:

  • 扫描所有输入目标文件,获取各个段的长度、属性和位置
  • 收集输入目标文件中的符号表,统一放到一个全局符号表中
  • 获得所有输入目标文件的段长度,将其合并,然后计算各个段合并之后的长度与位置,建立映射关系

3.符号解析和重定位(核心)

链接的第二步操作主要是,使用上一步收集到的信息,然后读取输入文件中的数据以及重定位信息,进行符号解析与重定位,调整代码中的地址
事实上,第二步是整个链接过程的核心,特别是重定位过程

这里来看一个实际例子,假设我们的代码中有a.c和b.c两个文件

//a.c
#include "stdio.h"

extern int glb_shared;

int main(void)
{
    int a = 100;
    swap(&a, &glb_shared);
}

//b.c
#include "stdio.h"

int glb_shared = 1;

void swap(int* a, int* b)
{
    *a = *b;
}

在链接前生成目标文件:
$ gcc -c -fno-stack-protector a.c b.c
//这里需要使用 -fno-stack-protector选项的原因是 fno-stack-protector是去除了stack的检测,我们直接是手动裸ld去链接,没有链接到“__stack_chk_fail"的所在库,不然会报错 ld: a.o: in function main': a.c:(.text+0x4f): undefined reference to__stack_chk_fail’

链接后生成目标文件
$ ld a.o b.o -e main -o ab

链接前后的目标文件对比:

读取连接前目标文件信息
$ objdump -h a.o 

a.o:     文件格式 elf64-x86-64

节:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000032  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  00000072  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000072  2**0
...

$ objdump -h b.o 

b.o:     文件格式 elf64-x86-64

节:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000001f  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000004  0000000000000000  0000000000000000  00000060  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000064  2**0

...


读取链接后目标文件:
$ objdump -h ab.o 

ab.o:     文件格式 elf64-x86-64

节:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .note.gnu.property 00000020  00000000004001c8  00000000004001c8  000001c8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .text         00000051  0000000000401000  0000000000401000  00001000  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .eh_frame     00000058  0000000000402000  0000000000402000  00002000  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .data         00000004  0000000000404000  0000000000404000  00003000  2**2

可以看到的信息:

  • VMA表示virtual memory address,表示虚拟地址,LMA代表load memory,运行地址,这里我们只关系虚拟地址;
  • 链接后的text段发生了重定位,从原来的0地址变为了 401000地址;
  • 在链接之前,所有目标文件的所有段VMA都是0,因为虚拟空间还没有被分配,在链接之后,ab中的各个段被分配到了相应的虚拟地址;

4.重定位表(undefined reference to xxx的根本原因)

前面说到了,链接之后ab中的各个段被分配到了相应的虚拟地址,但是想一下,链接器是怎么知道哪个变量被调整呢?
这是因为在ELF文件中,有一个重定位表(relocation table),这个表专门用来保存与重定位相关的信息,我们可以使用 objdump 来查看目标文件的重定位表:

$ objdump -r a.o 

a.o:     文件格式 elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
000000000000001a R_X86_64_PC32     glb_shared-0x0000000000000004
0000000000000027 R_X86_64_PLT32    swap-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text

能看到"a.o"中需要进行重定位的地方,即a.o所引用到的所有外部符号都有一个重定位地址。

也可使用 readelf命令 来查看各个目标文件的符号表:

//链接前
$ readelf -s a.o 

Symbol table '.symtab' contains 13 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS a.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     9: 0000000000000000    50 FUNC    GLOBAL DEFAULT    1 main
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND glb_shared
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND swap
//链接后
$ readelf -s ab.o

Symbol table '.symtab' contains 14 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000004001c8     0 SECTION LOCAL  DEFAULT    1 
     2: 0000000000401000     0 SECTION LOCAL  DEFAULT    2 
     3: 0000000000402000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000404000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS a.c
     7: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS b.c
     8: 0000000000401032    31 FUNC    GLOBAL DEFAULT    2 swap
     9: 0000000000404004     0 NOTYPE  GLOBAL DEFAULT    4 __bss_start
    10: 0000000000401000    50 FUNC    GLOBAL DEFAULT    2 main
    11: 0000000000404000     4 OBJECT  GLOBAL DEFAULT    4 glb_shared
    12: 0000000000404004     0 NOTYPE  GLOBAL DEFAULT    4 _edata
    13: 0000000000404008     0 NOTYPE  GLOBAL DEFAULT    4 _end

能看到在链接前,a.o的符号表中提示引用符号 glb_shared以及swap都是undefined,所以链接时也需要其他的目标文件来定义这些外部符号,而链接后的ab.o文件就有了这些符号的定义。
所以像我们常见的错误(undefined reference to xxxxx ):

$ ld a.o
ld: 警告: 无法找到项目符号 _start; 缺省为 0000000000401000
ld: a.o: in function `main':
a.c:(.text+0x1a): undefined reference to `glb_shared'
ld: a.c:(.text+0x27): undefined reference to `swap'

这些错误的根本原因是:
链接器会查找所有输入目标文件的符号表,组成一个全局符号表,然后进行重定位。上述错误在于没找到符号表

2.链接脚本

1.链接控制脚本

链接器一般都提供多种可以用来控制整个链接过程的方法,一般有以下几种方式:

  1. 使用命令行来给链接器指定参数,例如前面用的 ld -o -e等参数就属于这类;
  2. 集成好的IDE环境经常会将链接指令存放在目标文件里,让用户不用关心它;
  3. 使用链接脚本,这是最为灵活的链接控制方式;

当我们不指定链接脚本时,也会使用到默认的链接脚本,可以使用如下命令来查询默认链接脚本:

$ ld -verbose
GNU ld (GNU Binutils for Ubuntu) 2.34
 支持的仿真:
  elf_x86_64
  elf32_x86_64
  elf_i386
  elf_iamcu
  elf_l1om
  elf_k1om
  i386pep
  i386pe
使用内部链接脚本:
==================================================
/* Script for -z combreloc -z separate-code */
/* Copyright (C) 2014-2020 Free Software Foundation, Inc.
  Copying and distribution of this script, with or without modification,
  are permitted in any medium without royalty provided the copyright
  notice and this notice are preserved.  */
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
         "elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");
......

2.ld链接脚本语法简介

常见的命令语句如下:

命令语句 说明
ENTRY(symbol) 指定符号symbol为入口地址,ld有多种方法指定入口地址,优先级关系如下:ld命令行的 -e 选项 > 链接脚本的ENTRY(symbol)命令 > _start符号值 > .text段的第一字节地址 > 使用值0
STARTUP(filename) 将文件filename作为链接过程的第一个输入文件
SEARCH_DIR(path) 将路径path加入到链接器的库查找目录里
INPUT(file,file,…) 将指定文件作为链接过程的输入文件

SECTION命令最基本格式为:

SECTIONS
{
	....
	secname : { contents }
	...
}

contents规则,条件写法如下:

filename(sections)

其中,filename表示输入文件名,section表示段名,看几个具体例子:

  • file1.o(.data) :表示输入文件名为file1.o的文件中叫 .data段 的符合条件;
  • file1.o(.data .rodata) :表示输入文件名为file1.o的文件中叫 .data段.rodata段 的符合条件;
  • file1.o:表示file1.o的所有段都符合条件;
  • *(.data):所有输入文件中的.data文件符合条件;
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

操作系统系列(三)——编译和链接 的相关文章

随机推荐

  • 数学建模-启发式算法-蚁群算法

    文章目录 蚁群算法 算法原理 算法特点 算法步骤 流程图 代码 蚁群算法 蚁群算法 由Marco Dorigo于1992年在他的博士论文中提出 是一种灵感来源于蚂蚁在寻找食物过程中发现路径的行为 用来在图中寻找优化路径的算法 算法原理 蚂蚁
  • 单例模式中的懒汉模式和饿汉模式是什么?

    一 懒汉模式 顾名思义 他是一个懒汉 他不愿意动弹 什么时候需要吃饭了 他就什么时候开始想办法搞点食物 即懒汉式一开始不会实例化 什么时候用就什么时候new 才进行实例化 二 饿汉模式 顾名思义 他是一个饿汉 他很勤快就怕自己饿着 他总是先
  • sqldeveloper 格式化(美化)sql语句快捷键

    1 Ctrl A 全选需要格式化的sql 2 Ctrl F7 格式化
  • C语言文件操作(1)

    目录 一 为什么使用文件 二 什么是文件 2 1 程序文件 2 2 数据文件 2 3 文件名 三 文件的打开和关闭 3 1 文件指针 3 2 文件的打开和关闭 四 文件的顺序读写 4 1 对比一组函数 一 为什么使用文件 我们前面学习 结构
  • UVA 1601 The Morning after Halloween (BFS/双向BFS)

    单向BFS 1150ms include
  • opencv-bayes模型训练以及加载

    此代码适用于opencv 数据集分开训练数据集和测试数据集合 训练模型代码 Ptr
  • 分享5个免费的Python学习网站,抓紧收藏吧~

    最近有好多人说刚开始学习 有哪些免费的学习网站可以自学一下 于是 趁着空闲的时间在各大网站上面梳理了一下 找出了5个比较好的学习网站 并且都是免费的 比较适合初学者了解一些基础语法 解决BUG问题 如果是大佬的话了解一下就行了 废话不多说了
  • 基于RRT算法的避障路径规划(MATLAB代码)

    基于RRT算法的避障路径规划 MATLAB代码 路径规划是机器人导航和自主移动中的关键问题之一 Rapidly exploring Random Trees RRT 算法是一种经典的随机采样路径规划算法 它通过随机采样和不断扩展树结构来搜索
  • Java英文日期格式转换yyyy-MM-dd格式

    我们在后端的开发过程中会经常跟日期相关的类型打交道 不过我们大多数在开发过程中遇到的格式都是基本的 年 月 日 yyyy MM dd 格式 当然 这种格式的日期我们都可以用Java自带的SimpleDateFormat类自带的转换方法来进行
  • Animate 2021 for Mac(an 2021中文版) v21.0.1中文直装版

    全新的adobe animate2021版本更加的引入注目 比如经过修改后的资产面板允许您在新的 默认 和 自定义 选项卡中查找 组织和管理资产 并且可通过组合各种资产快速创建炫酷的动画 从而减轻了以往的逐个创建动画效果 Animate 2
  • 关于moment时区处理问题,指定时间转换特定时区

    如题网上一堆复制粘贴让使用timezone插件的文章 查了十几分钟 真是浪费生命 垃圾文章太多 如果只想转换某个时间而已 是不需要使用timezone插件的 这个插件一用可能全局的时区就变了 问题就大了 只转换某几个时间的时区解决办法是mo
  • echart时间轴设置详解

    timeline x center 时间轴X轴方向位置设置 y bottom 时间轴Y轴方向位置设置 width 80
  • 常见算法题(包括华为机试题)

    一 维护O 1 时间查找最大元素的栈 问题描述 一个栈stack 具有push和pop操作 其时间复杂度皆为O 1 设计算法max操作 求栈中的最大值 该操作的时间复杂度也要求为O 1 可以修改栈的存储方式 push pop的操作 但是要保
  • vue + element + CDN 的方式使用

    CDN方式开发vue项目步骤 1 cdn 链接相关css element ui css common css js jq vue js element ui js common js 等 2 每个页面嵌入 下列相关内容 div 3 1415
  • DOM驱动和数据驱动的区别

    引言 在前端开发中 DOM驱动和数据驱动是两种常见的开发模式 它们代表了不同的思维方式和开发方式 本文将深入探讨DOM驱动和数据驱动的区别 并通过代码详解它们在前端开发中的应用 1 DOM驱动 DOM驱动是传统的前端开发方式 它的核心思想是
  • jssip连freeswitch加webRtc相关控制

    highlight a11y light theme juejin 摘要 最近在做一个freeSwitch项目 前端需要通过sip协议完成音视频通话 把一些关键的核心api记录一下 因为网上找的一部分资料不一定准确 这个是实际操作过得具有一
  • DC系列漏洞靶场-渗透测试学习复现(DC-8)

    DC 8是一个易受攻击的实验环境 最终目的是让攻击者获得root权限 并读取flag 本篇文档中用到了shell反弹和exim提权 1 环境搭建 下载靶场文件 使用Vbox或者VM打开即可 攻击机使用kali 2020 2 主机发现 使用K
  • Qt打包Debug版本和Release版本(包含到其他电脑打不开,缺库问题等)含msvcp140d.dll,concrt140d.dll,vcruntime140d.dll等发布所需库文件

    首先Debug版本和Release区别 Release是发行版本 比Debug版本有一些优化 文件比Debug文件小 Debug是调试版本 Debug和Release调用两个不同的底层库 Debug是调试版本 包括的程序信息更多 只有Deb
  • C++string字符串查找和替换 string字符串查找和替换

    C string字符串查找和替换 string字符串查找和替换 功能描述 查找 查找指定字符串是否存在 替换 在指定的位置替换字符串 int find const string str int pos 0 const 查找str第一次出现位
  • 操作系统系列(三)——编译和链接

    往期地址 操作系统系列一 操作系统概述 操作系统系列二 进程 本期主题 编译和链接 文章目录 1 被隐藏了的过程 1 1 预编译 1 2 编译 1 3 汇编 1 4 链接 1 模块拼接 静态链接 2 空间地址与分配 3 符号解析和重定位 核