锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

哈工大2022计算机系统大作业——程序人生

时间:2023-10-22 12:07:00 各式连接器fce17

计算机系统

大作业

题 目 程序人生-Hello’s P2P

专 业 计算学部

学 号 120L021718

班 级 2003005

学 生 李骏成

指 导 教 师 吴锐

计算机科学与技术学院

2022年5月

摘 要

本文详细描述了hello.c是如何一步步生成的hello可执行文件,以及shell指令控制可执行文件运行的全过程。本文发掘IDE一键编译操作背后的故事:预处理、编译、汇编、链接,从计算机系统底部解释存储、内存分配、I/O该过程还解释了子过程的循环:创建和回收。以hello以生活为例,让人们了解计算机系统中普通事件背后未知的细节,帮助读者梳理漫游计算机系统。

关键词:编译、汇编、链接、过程、异常和信号

目 录

第1章 概述... - 5 -

1.1 Hello简介... - 5 -

1.2 环境与工具... - 5 -

1.2.1 硬件环境... - 5 -

1.2.2 软件环境... - 5 -

1.2.3 开发和调试工具... - 6 -

1.3 中间结果... - 6 -

1.4 本章小结... - 6 -

第2章 预处理... - 7 -

2.1 预处理的概念和作用... - 7 -

2.1.1 预处理的概念... - 7 -

2.1.2 预处理的作用... - 7 -

2.2在Ubuntu下一个预处理命令... - 7 -

2.3 Hello预处理结果分析... - 8 -

2.4 本章小结... - 9 -

第3章 编译... - 10 -

3.1 概念与作用的编译... - 10 -

3.1.1 编译的概念... - 10 -

3.1.2 编译的作用... - 10 -

3.2 在Ubuntu下编译的命令... - 10 -

3.3 Hello的编译结果解析... - 11 -

3.3.1 Hello.s头部内容... - 11 -

3.3.2 数据... - 12 -

3.3.3 赋值... - 13 -

3.3.4 类型转换... - 13 -

3.3.5 算数操作... - 14 -

3.3.6 关系操作... - 14 -

3.3.7 控制转移... - 15 -

3.3.8 数组操作... - 15 -

3.3.9 函数操作... - 16 -

3.4 本章小结... - 17 -

第4章 汇编... - 18 -

4.1 汇编的概念与作用... - 18 -

4.1.1 汇编的概念... - 18 -

4.1.2 汇编的作用... - 18 -

4.2 在Ubuntu下汇编的命令... - 18 -

4.3 可重定位目标elf格式... - 20 -

4.3.1 ELF头... - 20 -

4.3.2 节头部表... - 20 -

4.3.3 重定位信息... - 21 -

4.3.4 符号表... - 22 -

4.4 Hello.o的结果解析... - 22 -

4.4.1 分支转移... - 23 -

4.4.2 函数调用... - 23 -

4.4.3 字符串地址访问... - 24 -

4.5 本章小结... - 24 -

第5章 链接... - 25 -

5.1 链接的概念与作用... - 25 -

5.1.1 链接的概念... - 25 -

5.1.2 链接的作用... - 25 -

5.2 在Ubuntu下链接的命令... - 25 -

5.3 可执行目标文件hello的格式... - 26 -

5.3.1 ELF头... - 26 -

5.3.2 节头表... - 26 -

5.3.3 程序头表(段头表)... - 27 -

5.3.4 符号表... - 28 -

5.4 hello的虚拟地址空间... - 29 -

5.5 链接的重定位过程分析... - 30 -

5.5.1 链接过程... - 32 -

5.5.2 重定位过程... - 32 -

5.6 hello的执行流程... - 33 -

5.7 Hello的动态链接分析... - 33 -

5.8 本章小结... - 34 -

第6章 hello进程管理... - 35 -

6.1 进程的概念与作用... - 35 -

6.1.1 进程的概念... - 35 -

6.1.2 进程的作用... - 35 -

6.2 简述壳Shell-bash的作用与处理流程... - 35 -

6.2.1 Shell-bash的作用... - 35 -

6.2.2 Shell-bash的处理流程... - 35 -

6.3 Hello的fork进程创建过程... - 36 -

6.4 Hello的execve过程... - 36 -

6.5 Hello的进程执行... - 37 -

6.6 hello的异常与信号处理... - 39 -

6.7本章小结... - 42 -

第7章 hello的存储管理... - 43 -

7.1 hello的存储器地址空间... - 43 -

7.1.1 逻辑地址... - 43 -

7.1.2 线性地址... - 43 -

7.1.3 虚拟地址... - 43 -

7.1.4 物理地址... - 43 -

7.2 Intel逻辑地址到线性地址的变换-段式管理... - 43 -

7.3 Hello的线性地址到物理地址的变换-页式管理... - 45 -

7.4 TLB与四级页表支持下的VA到PA的变换... - 46 -

7.5 三级Cache支持下的物理内存访问... - 47 -

7.6 hello进程fork时的内存映射... - 48 -

7.7 hello进程execve时的内存映射... - 48 -

7.8 缺页故障与缺页中断处理... - 48 -

7.8.1 缺页故障... - 49 -

7.8.2 缺页中断处理... - 49 -

7.9动态存储分配管理... - 49 -

7.9.1 动态分配器... - 49 -

7.9.2 带边界标签的隐式空闲链表分配器... - 50 -

7.9.3 显式空闲链表... - 51 -

7.10本章小结... - 51 -

第8章 hello的IO管理... - 52 -

8.1 Linux的IO设备管理方法... - 52 -

8.2 简述Unix IO接口及其函数... - 52 -

8.2.1 Unix I/O接口... - 52 -

8.2.2 Unix I/O函数... - 52 -

8.3 printf的实现分析... - 53 -

8.4 getchar的实现分析... - 55 -

8.5本章小结... - 55 -

结论... - 56 -

附件... - 57 -

参考文献... - 58 -

第1章 概述

1.1 Hello简介

1.Hello的P2P过程:P2P具体是指From Program to Process,指的是从高级语言源程序通过预处理、编译、汇编、链接这些阶段,生成可执行的目标文件。

①预处理:预处理器(Preprocessor)处理以#开始的预编译指令,如宏定义(#define)、头文件引用(#include)、条件编译(#ifdef)等,具体是指将高级语言源程序的.c文件变成ASCII编码的高级语言源程序的.i文件。

②编译:使用编译器(Compiler)将C语言,翻译成为汇编代码,将高级语言源程序的.i文件变为汇编语言源程序的.s文件。

③汇编:使用汇编器(Assembler)将汇编代码翻译成二进制机器语言,将汇编语言源程序的.s文件转化为机器语言的.o文件(可重定位的目标文件)。

④链接:使用链接器(Linker)将汇编器生成的目标文件外加库链接为一个可执行文件。

2.Hello的020过程:020具体指的是From Zero-0 to Zero-0,指的是一开始没有Hello相关的进程,shell通过fork创建了一个子进程,在子进程中通过execve运行Hello程序,映射虚拟内存,将Hello程序相关内容存入到物理内存中。在Hello运行结束后,子进程终止,创建这个子进程的父进程回收子进程,删除子进程相关的内容,将与子进程有关的全部清空。所以对于Hello程序来说,运行前是没有相关进程的,运行后子进程被回收,也没有相关进程,所以是从0到0,即From Zero-0 to Zero-0。

1.2 环境与工具

1.2.1 硬件环境

                     i5-8265U X64 CPU; 1.80GHz; 8G RAM; 512GHD Disk

1.2.2 软件环境

                     Windows 10 64位; VMware Workstation Pro 16.2.0; Ubuntu 20.04.0

1.2.3 开发与调试工具

vim/gedit+gcc; gdb; edb; objdump; readelf; Visual Studio Code; wxHexEditor; CodeBlocks;

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

1.4 本章小结

本章是整篇论文的概述,首先介绍了Hello的P2P和020过程,描述了Hello程序编译和运行时的具体细节,还说明了编写本论文、分析和调试程序使用的软硬件环境,以及开发与调试工具,最后列举出了为了编写本论文,生成的中间结果文件的名字、文件的作用等。

第2章 预处理

2.1 预处理的概念与作用

预处理具体的过程是,将高级语言源程序的.c文件转化为ASCII编码的高级语言源程序.i文件,得到的.i文件用于后续步骤。

2.1.1 预处理的概念

预处理的概念是预处理器(preprocessor)在源代码编译之前对其进行一些文本性质的操作,操作的对象为原始代码中以字符#开头的命令,包括#include的头文件、#define的宏定义,#if、#ifdef、#endif等条件编译,得到的结果再由编译器核心进一步编译。

2.1.2 预处理的作用

预处理的作用是将程序中宏定义的变量名替换为对应的值、通过文件包含将多个源文件连接成一个源文件(有助于后续编译)、进行条件编译即只允许后续编译过程中只编译源程序中满足条件的程序段。

预处理阶段主要作用和目的是让编译器在随后对文本进行编译的过程中,更加方便,因为访问库函数这类操作在预处理阶段已经完成,减少了编译器的工作。

2.2在Ubuntu下预处理的命令

预处理的命令为gcc -E hello.c -o hello.i

输入后如图2-1所示:

图2-1  预处理命令执行结果

2.3 Hello的预处理结果解析

在Hello.c程序中,能够被预处理器处理的内容有注释和三条对库文件的引用,图2-2是它们的具体内容。

图2-2  Hello程序中在预处理中被处理的部分

①对于注释的内容,在预处理过程中是直接删掉的,所以不会显示在.i文件中。

②对于“#include ”命令,预处理直接解析了对stdio.h库文件的调用路径,然后存储在.i文件中,还将程序中需要的stdio.h中的代码补充到.i文件中,预处理结果如下图2-3所示。

图2-3  “#include ”的预处理结果

③对于“#include ”命令,预处理直接解析了对unistd.h库文件的调用路径,然后存储在.i文件中,还将程序中需要的unitstd.h中的代码补充到.i文件中,预处理结果如下图2-4所示。

图2-4  “#include ”的预处理结果

④对于“#include ”命令,预处理直接解析了对stdlib.h库文件的调用路径,然后存储在.i文件中,还将程序中需要的stdlib.h中的代码补充到.i文件中,预处理结果如下图2-5所示。

图2-5  “#include ”的预处理结果

2.4 本章小结

本章介绍了预处理的概念和作用,并且说明了Ubuntu下的预处理命令,展示和解析了Hello.c文件使用预处理命令后的结果。

预处理是从高级语言源程序到可执行文件的第一步,它的作用主要是为后面的编译过程奠基,去除无用部分,将宏定义的变量直接替换成对应值,找到引用库文件的路径,将程序中涉及到的库中的代码补充到程序中,最后将初步处理完成的文本保存在hello.i中,方便以后的内核器件直接使用。

第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念

编译是指编译器将预处理后的ASCII编码的高级语言源程序文件(.i文件),通过编译程序生成汇编语言目标程序,即.s文件。此阶段编译器会完成对代码的语法和语义的分析,生成汇编代码,并将这个代码保存在hello.s文件中。

3.1.2 编译的作用

编译的基本作用是把高级语言源程序翻译成汇编语言目标程序,还应具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人-机联系等重要作用。

①语法检查:检查源程序是否合乎语法。如果不符合语法,编译程序要指出语法错误的部位、性质和有关信息。

②调试措施:检查源程序是否合乎设计者的意图。编译程序在目标程序运行时能输出程序动态执行情况的信息,这些信息有助于用户核实和验证源程序是否表达了算法要求。

③修改手段:为用户提供简便的修改源程序的手段。编译程序通常要提供批量修改手段(用于修改数量较大或临时不易修改的错误)和现场修改手段(用于运行时修改数量较少、临时易改的错误)。

④覆盖处理:主要是为处理程序长、数据量大的大型问题程序而设置的。

⑤目标程序优化:提高目标程序的质量,即占用的存储空间少,程序的运行时间短。依据优化目标的不同,编译程序可选择实现表达式优化、循环优化或程序全局优化。

⑥不同语言合用:其功能有助于用户利用多种程序设计语言编写应用程序或套用已有的不同语言书写的程序模块。最为常见的是高级语言和汇编语言的合用。

⑦人机联系:确定编译程序实现方案时达到精心设计的功能。目的是便于用户在编译和运行阶段及时了解内部工作情况,有效地监督、控制系统的运行。

3.2 在Ubuntu下编译的命令

编译的命令为gcc -S hello.i -o hello.s

输入后结果如图3-1所示:

图3-1  编译命令执行结果

3.3 Hello的编译结果解析

3.3.1 Hello.s头部内容

指令

含义

.file

C文件声明

.text

代码段

.globl

声明全局变量

.data

已初始化的全局和静态C变量

.align

声明对指令或者数据的存放地址进行对齐的方式

.type

指明函数类型或对象类型

.string

声明string型数据

.section .rodata

只读数据段

图3-2  Hello.s头部段

通过Hello.s文件的第一行可以知道,这个汇编语言目标程序是从Hello.c文件转化过来的。从第四行的.align 8可以知道,变量在内存中存储是以8字节对齐的。

第五行的.LC0是一个标记,代表了下面这个字符串的地址,第六行的字符串代表的是程序中“用法: Hello 学号 姓名 秒数!\n”这句话,在汇编语言中,每个汉字是由大于等于3个字节的数字表示的,所以可以知道一个汉字在Hello.s中是用3个字节的数字和“\”符号表示的。

第七行的.LC1也是一个标记,代表了下面这个字符串的地址,第八行的字符串表示的是程序中“Hello %s %s\n”这句话。

第十行表示程序中的全局变量为main。第十一行表示程序中的函数有main。

Hello.s的头部段大概记录程序的一些内容。

3.3.2 数据

Hello.s中出现的数据类型包括整数、数组、字符串。

1.整数

1int i

i是for循环中的变量,虽然在main函数一开始就定义了,但是在后面for循环中才赋值为0,然后每次循环加一的,通过图3-3和图3-4就可以分析出i是存储在内存中的,它的存储位置为%rbp-4。

图3-3  i的声明和赋值为0

图3-4  i的加一

       2)int argc

argc是main函数的形参,虽然在参数传递的时候没有明确传递argc,但是它表示的是argv参数列表中元素的个数的,argc主要是在程序后方的条件判断中使用的,将argc与4进行比较,通过图3-5可以知道,argc是存储在内存中的,它的存储位置为%rbp-20。

图3-5  argc的表示

       2.数组

char *argv[]

char *argv[]为字符串数组,在这个数组中每个单元存储的是一个char *类型的变量,char *指的是字符类型的指针,即为一个字符串的地址,所以这个数组的每个单元大小为8字节,数组的起始地址为argv。通过图3-6可见,argv的值为内存中地址为%rbp-32的内容,故内存中地址为-32(%rbp)+16的8字节的内容为argv[2],内存中地址为-32(%rbp)+8的8字节的内容为argv[1]。

图3-6  数组char *argv[]的表示

       3.字符串

1用法: Hello 学号 姓名 秒数!\n”

这个字符串是作为printf函数的第一个参数的,它的存储地址是用相对寻址方式表示的,存放在只读数据段.rodata中,可以发现字符串被编码成utf-8格式,一个汉字在utf-8编码中占三个字节,一个\代表一个字节。这个字符串的具体表示见图3-7。

图3-7  “用法: Hello 学号 姓名 秒数!\n”具体表示

2)“Hello %s %s\n

第二个printf传入的输出格式化参数,存放在只读数据段.rodata中。如图3-8所示。

图3-8  “Hello %s %s\n”具体表示

3.3.3 赋值

i=0

i是保存在栈中的局部变量,直接用mov语句对i进行赋值,因为i是4B即32b,所以用movl对i进行赋值。如图3-9所示。

图3-9  i=0赋值操作

3.3.4 类型转换

设计显式类型转换的是atoi(argv[3]),此处是进行一个从字符串到整数的类型转换。这种类型转换要求字符串中必须全是数字型的字符,只有这种情况才能转换成为一个整数。

在汇编语言中,argv[3]的地址为-32(%rbp)+24,argv[3]会作为参数传递进入函数atoi中,函数atoi的返回值存放在寄存器%eax中,作为函数sleep的参数进行传递。类型转换的具体过程如图3-10。

图3-10  类型转换atoi函数的表示

3.3.5 算数操作

1)for循环中i++

自增指令在汇编语言中被解释为被操作数加一,采用add指令,因为i是4B即32b,所以用addl对i进行增加,如图3-11所示。

图3-11  i++指令表示

2)leaq计算“Hello %s %s\n”字符串的地址

for循环内部需要对“Hello %s %s\n”字符串进行打印,使用了加载有效地址指令leaq计算字符串的地址%rip+.LC1并传递给%rdi,可以看出这个字符串使用的是相对寻址方式,一般全局变量和指令跳转时大多使用相对寻址方式。具体如图3-12所示。

图3-12  leaq计算“Hello %s %s\n”字符串的地址的表示

3.3.6 关系操作

总结进行关系操作的指令如下:

指令

基于

解释

CMP S1, S2

S2-S1

比较设置条件码

TEST S1, S2

S1&S2

测试设置条件码

SET** D

D=**

按照**将条件码设置D

J**

——

根据**与条件码进行跳转

程序中涉及的关系运算如下:

1)argc!=4

在Hello.s中表示判断argc是否等于4,将argc即-20(%rbp)减去4,设置条件码,然后通过条件码判断差值是否为0,再决定是否进行跳转。具体如图3-13。

图3-13  argc!=4的汇编表示

2)i < 8

在Hello.s中表示判断i是否小于8,将i的值即-4(%rbp)减去7,设置条件码,通过条件码判断i是否小于等于7,再决定之后如何跳转。具体表示见图3-14。

图3-14  i<8的汇编表示

3.3.7 控制转移

1)if(argc!=4)

在24行处,先计算argc即-20(%rbp)减去4,设置条件码,然后25行通过判断条件码决定如何进行跳转。如果argc等于4,则满足25行的条件,程序将会跳转到.L2处,如果argc不等于4,则继续运行,输出地址为.LC0+%rip处的字符串,结束程序。条件转移的具体表示如图3-15所示。

图3-15  if(argc!=4)的汇编表示

2)for(i=0; i<8; i++)

使用变量i循环8次。首先无条件跳转到位于循环体.L3之后的比较代码,使用cmpl进行比较,如果i<=7,则跳入.L4 for循环体执行,否则说明循环结束,顺序执行for之后的逻辑。如图3-16所示。

图3-16  for(i=0; i<8; i++)的汇编表示

3.3.8 数组操作

这个程序中出现的数组为字符串型数组char *argv[],每个单元为8个字节,每个单元中存放的是字符串的地址。其中argv为这个数组的地址,即为这个数组第一个元素的地址,argv为-32(%rbp),根据数组地址的计算方法可以得到argv[1]、argv[2]、argv[3]的地址。数组地址的计算方法为&(argv[a])=argv+a*8。

argv[1]和argv[2]的地址在Hello.s中具体的表示过程描述如下。

argv[1]: 数组首地址存放于-32(%rbp),先将其存储到%rax中,再加上偏移量$16,再将该位置内容放在%rdx中,成为下一个函数的第一个参数。

argv[2]: 数组首地址存放于-32(%rbp),先将其存储到%rax中,再加上偏移量$8,再将该位置内容放在%rdi中,成为下一个函数的第二个参数。

汇编代码过程如图3-17所示。

图3-17  argv[1]和argv[2]的数组操作

3.3.9 函数操作

Hello.c中涉及的函数操作有:

  1. main函数:
  1. 传递控制:

main函数因为被调用call才能执行(被系统启动函数__libc_start_main调用),call指令将下一条指令的地址压栈,然后跳转到main函数。

  1. 传递数据:

外部调用过程向main函数传递参数argc和argv,分别使用%edi和%rsi存储,函数正常出口为return 0,将%eax设置0返回。

  1. 分配和释放内存:

使用%rbp记录栈帧的底,函数分配栈帧空间在%rbp之上,程序结束时,调用leave指令,leave相当于

mov %rbp,%rsp

pop %rbp

恢复栈空间为调用之前的状态,然后ret返回,ret相当pop IP。

  1. printf函数:
  1. 传递数据:

第一次printf将%rdi设置为“用法: Hello 学号 姓名 秒数!\n”字符串的首地址。第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rdx为argv[1],%rsi为argv[2]。

  1. 控制传递:

第一次printf因为只有一个字符串参数,所以call puts@PLT;第二次printf使用call printf@PLT。

  1. exit函数:
  1. 传递数据

将%edi设置为1。

  1. 控制传递

call exit@PLT。

  1. sleep函数:
  1. 传递数据

将%edi设置为argv[3]。

  1. 控制传递

call sleep@PLT。

  1. getchar函数:
  1. 控制传递

call gethcar@PLT

3.4 本章小结

本章主要介绍了有关编译的概念作用,然后使用编译指令生成了编译后的文件。对于生成的.s文件,分析了C语言的数据与操作在机器之中如何被处理翻译的,明确了汇编文件中的每一步都是有何意义,为下一步汇编打下了基础。

第4章 汇编

4.1 汇编的概念与作用

4.1.1 汇编的概念

汇编指的是汇编器(Assembler)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标文件,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,包含hello程序执行的机器指令。

4.1.2 汇编的作用

将在hello.s中保存的汇编代码翻译成可以供机器执行的二进制代码,使之在链接后能够被计算机直接执行,为之后的链接和运行奠定基础。

4.2 在Ubuntu下汇编的命令

汇编命令为gcc -c hello.s -o hello.o

输入后结果如图4-1所示:

图4-1  汇编命令执行结果

使用winHex打开hello.o,显示如图4-2所示:

图4-2  使用winHex打开Hello.o

4.3 可重定位目标elf格式

使用命令 readelf -a hello.o > hello_o_elf.txt,读取hello.o文件的ELF格式至hello_o_elf_txt中。

4.3.1 ELF头

ELF头以一个16字节的序列开始,描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行、共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。如图4-3所示。

图4-3  Hello.o的ELF头

4.3.2 节头部表

节头部表包括节的全部信息,如图4-4所示,各个节的名称及内容如下:

节名称

包含内容

.text

已编译程序的机器代码

.rela.text

一个.text节中位置的列表,链接器链接其他文件时,需修改这些内容

.data

已初始化的全局和静态C变量

.bss

未初始化的全局和静态C变量和所有被初始化为0的全局或静态变量

.rodata

只读数据段

.comment

包含版本控制信息

.note.GNU-stack

包含注释信息,有独立的格式

.symtab

符号表,存放程序中定义和引用的函数和全局变量信息

.strtab

字符串表,包括.symtab和.debug节中的符号表以及节头部中的节名字

.shstrtab

包含节区名称

图4-4  Hello.o节头部表

4.3.3 重定位信息

重定位是将EFL文件中的未定义符号关联到有效值的处理过程。在Hello.o中,对printf,exit等函数的未定义的引用替换为该进程的虚拟地址空间中机器代码所在的地址。如图4-5所示。

图4-5  Hello.o的重定位信息

4.3.4 符号表

符号表(.symtab)是用来存放程序中定义和引用的函数和全局变量的信息。重定位需要引用的符号都在其中声明。如图4-6所示。

图4-6  Hello.o的符号表

4.4 Hello.o的结果解析

使用 objdump -d -r hello.o > helloo.objdump获得反汇编代码。

对比hello.s中main函数与反汇编后main函数如图4-7所示。

图4-7  编译结果和反汇编结果对比左侧为Hello.s,右侧为Helloo.objdump

两者的内容上差别不大,具体有如下的几个区别:

4.4.1 分支转移

反汇编代码跳转指令的操作数使用的不是段名称如.L2,段名称只是在汇编语言中便于编写的助记符,在汇编成机器语言之后使用地是确定的地址。

4.4.2 函数调用

在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,在链接后再进一步确定。

4.4.3 字符串地址访问

在.s文件中,访问.rodata(printf中的字符串),使用段名称+%rip,在反汇编代码中0+%rip,因为.rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。

4.5 本章小结

本章介绍了hello从hello.s到hello.o的汇编过程,通过查看hello.o的ELF格式和使用objdump得到反汇编代码与hello.s进行比较的方式,间接了解到从汇编语言映射到机器语言汇编器需要实现的转换。

第5章 链接

5.1 链接的概念与作用

5.1.1 链接的概念

链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行与运行时(run time),也就是有应用程序来执行。

5.1.2 链接的作用

链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够按操作系统装入执行的统一整体。

通过链接可以实现将头文件中引用的函数并入到程序中,解析未定义的符号引用,将目标文件中的占位符替换为符号的地址。完成程序中各目标文件的地址空间的组织,这可能涉及重定位工作。

5.2 在Ubuntu下链接的命令

链接的命令为ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

输入链接命令后的结果如图5-1所示:

图5-1  输入链接命令后的结果

5.3 可执行目标文件hello的格式

输入readelf -a hello > hello.elf命令,生成hello可执行文件的elf格式信息,存入文件hello.elf中。

5.3.1 ELF头

通过ELF头可以知道文件类型、机器类型、版本、ELF头大小等信息,能够看到文件类型变成了EXEC(可执行文件),还可以知道节头表的地址和节头表中的条目数量,我们可以看到可执行文件hello中的节头表中的项目比hello.o中节头表的项目更多,说明链接上了其他的代码,节变多了。ELF头具体信息可见图5-2。

图5-2  Hello的ELF头信息

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章