资讯详情

HIT 计算机系统大作业

该论文以hello.c文件是研究对象,结合对计算机系统的深入理解和课堂教师的教学,Ubuntu系统下对hello通过研究程序的整个生命周期,研究了程序hello.c本学期对计算机系统课程所学知识进行了梳理和回顾,加深了对计算机系统的理解。

底层原理;代码周期;计算机系统;编译;加载;链接;终止;回收;……

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

2.1 预处理的概念和作用

2.2在Ubuntu下一个预处理命令

2.3 Hello预处理结果分析

2.4 本章小结

3.1 概念与作用的编译

3.2 在Ubuntu下面编译的命令

3.3 Hello分析编译结果

3.4 本章小结

4.1 汇编的概念和功能

4.2 在Ubuntu下面汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

5.1 链接的概念和功能

5.2 在Ubuntu下链接命令

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

5.4 hello虚拟地址空间

5.5 链接重定位过程分析

5.6 hello的执行程

5.7 Hello的动态链接分析

5.8 本章小结

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

7.1 hello的存储器地址空间 - 36 -

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

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

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

7.8 缺页故障与缺页中断处理

7.9动态存储分配管理

7.10本章小结

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

1.1 Hello简介

P2P(From Program to Process):将Hello从程序文件到执行中的文件的过程。在这其中,hello.c依次经过了预处理,编译,汇编,链接成为可执行文件(Program)。我们打开命令行窗口中,打开文件路径,输入./hello就可以执行该文件。

020:shell首先fork一个子进程,然后通过execve加载并执行hello,映射虚拟内存,进入程序入口后将程序载入物理内存,进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。即,从0开始,以0结束,为020。

1.2 环境与工具

  1. 硬件环境

Intel Core i7-8550U X64 CPU; 1.80GHz; 16G RAM; 128G SSD + 1T HDD;

  1. 软件环境

Windows 10 64位; VMware 15 pro; Ubuntu-18.04.3 LTS;

  1. 开发与调试工具

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

1.3 中间结果

中间结果文件

文件作用

hello.i

预处理得到的文件

ASCII码的中间文件

hello.s

ASCII汇编语言文件

hello.o

    as得到可重定位目标文件

Disas_hello.s

反汇编得到的文本文件

elf.txt

用readelf读取hello.o得到的ELF格式信息

hello

ld得到可执行目标文件

hello_5.3.elf

hello的elf文件

hello_objdump.s

hello的反汇编文件

1.4 本章小结

      本章对hello进行了一个总体的概括,首先介绍了P2P、020的意义和过程,介绍了作业中的硬件环境、软件环境和开发工具,最后简述了从.c文件到可执行文件中间经历的过程。

2.1 预处理的概念与作用

预处理的概念:

预处理器(cpp)根据以字符#开头的命令,修改原始的C程序,试图解释为预处理指令(preprocessing directive) 。

ISO C/C++要求支持的预处理指令包括:

#if、 #ifdef、 #ifndef、 #else、 #elif、 #endif(条件编译)、 #define(宏定义)、 #include(源文件包含)、 #line(行控制)、 #error(错误指令)、 #pragma(和实现相关的杂注)以及单独的#(空指令)。

预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。

预处理的作用:

宏定义:宏定义将代码中的宏名与实际字符串进行替换,可以增强代码的可读性;

文件包含处理:将include头文件复制到#处替换,减少重复的工作,加强代码模块化;

条件编译处理:决定哪些具体代码会被编译处理。

2在Ubuntu下预处理的命令

命令 gcc -E hello.c -o hello.i

输入后如图2-1所示

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

2.3 Hello的预处理结果解析

在Linux下打开hello.i文件,可以发现hello.i程序已经拓展为3110行,行数比起hello.c文件大幅增加。其中, hello.c中的main函数相关代码在hello.i程序中对应着3094行到3110行。

图2-2 打开hello.i文件

在main函数内代码出现之前是大段的头文件 stdio.h unistd.h stdlib.h 的依次展开。展开的具体流程概述如下(以stdio.h为例):CPP先删除指令#include <stdio.h>,并到Ubuntu系统的默认的环境变量中寻找 stdio.h,最终打开路径/usr/include/stdio.h下的stdio.h文件。若stdio.h文件中使用了#define语句,则按照上述流程继续递归地展开,直到所有#define语句都被解释替换掉为止。除此之外,CPP还会进行删除程序中的注释和多余的空白字符等操作,并对一些值进行替换。

2.4 本章小结

    本章主要介绍了预处理(包括头文件的展开、宏替换、去掉注释、条件编译)的概念及作用,以及Linux下预处理的两个指令,同时结合Ubuntu系统下hello.c文件实际预处理之后得到的hello.i程序并对预处理结果进行了解析,详细了解了预处理的内涵。

3.1 编译的概念与作用

将代码转换为汇编指令或者机器指令的过程。即编译器通过词法分析和语法分析,将合法指令翻译为等价的汇编代码。(不同语言通过编译后的到的文件内容可能相同)        

3.2 在Ubuntu下编译的命令

参考PPT,在Ubuntu系统下,进行预处理的命令为:

gcc –m64 –no-pie –fno-PIC -S -o hello.i hello.s

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

3.3 Hello的编译结果解析

  1. 汇编指令

对hello.s文件整体结构分析如下:

指令

含义

.file

C文件声明

.text

代码段

.globl

声明全局变量

.data

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

.align 4

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

.type

指明函数类型或对象类型

.size

声明变量大小

.long .string

声明long型、string型数据

.section .rodata

只读数据段

  1. 数据

在hello.s中,涉及的数据类型包括以下三种:整数,字符串,数组。下面对每种数据类型依次进行分析。

在hello.s中,涉及的整数有:

  1. int sleepsecs

查看C语言文件可知,sleepsecs为int型全局变量,已被初始化赋值2.5。

图3-2 初始化赋值sleepsecs

经过编译阶段得到的hello.s文件中,编译器在.text段中将sleepsecs声明为全局变量,在.type段声明其为object类型,在.size段声明其长度为4,设置其值为2。具体情况如下:

图3-3 声明变量长度

  1. int i

编译器将局部变量存储在寄存器或者栈空间中。i作为函数内部的局部变量,并不占用文件实际节的空间,只存在于运行时栈中。对于i的操作就是直接对寄存器或栈进行操作。

在hello.s中我们可以看出,i占据了4字节的地址空间:

图3-4 i字节地址

  1. int argc

argc作为第一个参数传入,下图结合汇编代码分析argc保存位置。

图3-5 argc保存位置

  1. 立即数3

立即数3在汇编语句中直接以$3的形式出现

1.“Usage: Hello 学号 姓名!\n”

第一个printf传入的输出格式化参数,存放在只读数据段.rodata中,可以发现字符串被编码成utf-8格式,一个汉字在utf-8编码中占三个字节,一个\代表一个字节。如图所示。

图3-6  Usage: Hello 学号 姓名!\n保存位置

2."Hello %s %s\n"

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

图3-7 Hello %s %s\n保存位置

char *argv[]

argv单个元素char*大小为8位,argv指针指向已经分配好的、一片存放着字符指针的连续空间,起始地址为argv。下图分析了argv传入函数时存储的位置

main函数中访问数组元素argv[1],argv[2]时,按照起始地址argv大小8位计算数据地址取数据,在hello.s中,使用两次(%rax)(两次rax分别为argv[1]和argv[2]的地址)取出其值。

图3-8 sleepsecs赋值信息

3.3.3赋值

  1. int sleepsecs=2.5

在C语言源程序中包含一个隐式类型转换:将2.5赋值给一个int类型,结果为2。体现在hello.s中,直接在.data节中将sleepsecs声明为值2的long类型数据。

  1. int i

对局部变量的赋值在汇编代码中通过mov指令完成。具体使用哪条mov指令由数据的大小决定,如图所示:

表格 3 mov指令的后缀

后缀

b

w

l

q

大小(字节)

1

2

3

4

3.3.4类型转换

涉及隐式类型转换的是:int sleepsecs=2.5,将浮点数类型的2.5转换为int类型。

当在double或float向int进行类型转换的时候,程序改变数值和位模式时会向零舍入。例如1.999将被转换成1,-1.999将被转换成-1。进一步来讲,可能会产生值溢出的情况,与Intel兼容的微处理器指定位模式[10…000]为整数不确定值,一个浮点数到整数的转换,如果不能为该浮点数找到一个合适的整数近似值,就会产生一个整数不确定值。

浮点数默认类型为double,所以上述强制转化是double强制转化为int类型。遵从向零舍入的原则,将2.5舍入为2。

3.3.5算数操作

汇编语言中,算数操作的指令包括:

指令

效果

leaq s,d

d=&s

inc d

d+=1

dec d

d-=1

neg d

d=-d

add s,d

d=d+s

sub s,d

d=d-s

imulq s

r[%rdx]:r[%rax]=s*r[%rax](有符号)

mulq s

r[%rdx]:r[%rax]=s*r[%rax](无符号)

idivq s

r[%rdx]=r[%rdx]:r[%rax] mod s(有符号) r[%rax]=r[%rdx]:r[%rax] div s

divq s

r[%rdx]=r[%rdx]:r[%rax] mod s(无符号) r[%rax]=r[%rdx]:r[%rax] div s

在hello.s中,具体涉及的算数操作包括:

  1. subq $32, %rsp:开辟栈帧
  2. addq $16, %rax:修改地址偏移量
  3. addl $1, -4(%rbp):实现i++的操作

图3-9 hello.s中涉及的算数操作

3.3.6 关系操作

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

指令

基于

解释

CMP S1, S2

S2-S1

比较设置条件码

TEST S1, S2

S1&S2

测试设置条件码

SET** D

D=**

按照**将条件码设置D

J**

——

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

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

(1)argc!=3;是在一条件语句中的条件判断:argc!=3,进行编译时,这条指令被编译为:cmpl $3,-20(%rbp),同时这条cmpl的指令还有设置条件码的作用,当根据条件码来判断是否需要跳转到分支中。

(2)i < 8,在hello.c作为判断循环条件,在汇编代码被编译为:cmpl $9,-4(%rbp),计算 i-7然后设置 条件码,为下一步 jle 利用条件码进行跳转做准备。

3.3.7控制转移

首先设置条件码,然后根据条件码来进行控制转移。hello.c中有以下控制转移指令:

(1)判断i是否为3,如果i等于3,则不执行if语句,否则执行if语句:

  1. for(i = 0;i < 8;i++),通过每次判断i是否满足小于8来判断是否需要跳转至循环语句中:

第一处画圈:i赋初值0,然后无条件跳转至判断条件的代码中

第二处画圈:判断i是否符合循环的条件,符合直接跳转至循环体的内部,即L4

3.3.8数组操作

c源程序中的数组操作出现在循环体for循环中,每次循环中都要访问argv[1],argv[2]这两个内存。在翻译时,argv[]先是被存在用户栈中,再使用基址加偏移量寻址访问argv[1],argv[2]。

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

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

程序中涉及的数组为char *argv[],即函数的第二个参数。在hello.s中,其首地址保存在栈中。访问时,通过寄存器寻址的方式访问。

3.3.9 函数操作           

调用函数时有以下操作:

(假设函数P调用函数Q)

  1. 传递控制:进行过程 Q 的时候,程序计数器设置为 Q 的代码的起始地址;返回时,把程序计数器设置为 P 中调用 Q 后面那条指令的地址。
  2. 传递数据:P 能够向 Q 提供一个或多个参数,而Q 能够向 P 中返回一个返回值。

(3) 分配和释放内存:开始时,Q 可能需要为局部变量分配空间,而在返回前需释放这些空间。

hello.c程序中涉及的函数操作有:

main函数,printf,exit,sleep ,getchar函数

main函数的参数是argc和argv;两次printf函数的参数恰好是那两个字符串

exit参数是1,sleep函数参数是atoi(argv[3])

函数的返回值存储在%eax寄存器中。

3.4 本章小结

围绕hello.s文件,介绍编译概念,分析了hello.o文件中代码结构以及文件结构,对其汇编语言里的数据类型,条件控制,数组,字符串等等在汇编语言中的操作进行了较为详细的解释。

4.1 汇编的概念与作用

概念:将汇编语言转化为可重定位目标文件,生成hello.o文件,是二进制编码文件,包含程序的机器码指令,就是把汇编语言转化为机器语言

作用:转化为机器可识别的语言,并将相关指令以可重定位目标程序格式保存在.o文件中

4.2 在Ubuntu下汇编的命令

命令 gcc -c hello.s -o hello.o

4-1编译命令执行结果

图4-2 使用wxHexEditor打开hello.o

4.3 可重定位目标elf格式

ELF(Executable and Linkable Format) 可执行,可链接文件格式,每个section再映射时的长度都是系统页的整数倍,

存在segment 和section两种试图,segment是section的集合

操作系统往往十一页为基本单位来管理内存分配,一般页的大小为4096B,4KB大小。内存管理的权限管理的粒度也是以页为单位,页内的内存具有相同的权限等属性,并且操作系统对页的管理往往追求高效和高利用率,ELF加载到内存中,再映射时,是以系统的页长度为单位,每个section映射长度是系统页的整数倍,如果Section的长度不是整数倍,导致多余的部分也将占用一个页。当一个ELF文件具有很多Section,就会导致内存浪费严重,这样可以减少内部的碎片,节省空间,显著提高内存利用利用率。所以在执行时会集合成Segment。

图4-3关于EFL的详细解释

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

  1. ELF头

以 16字节序列 Magic 开始,其描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等相关信息。

图4-4 EFL头信息

  1. 节头部表

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

节名称

包含内容

.text

已编译程序的机器代码

.rela.text

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

.data

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

.bss

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

.rodata

只读数据段

.comment

包含版本控制信息

.note.GNU-stack

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

.symtab

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

.strtab

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

.shstrtab

包含节区名称

图4-5 EFL节头信息

  1. 重定位信息

重定位是将EFL文件中的未定义符号关联到有效值的处理过程。在hello.o中,对printf,exit等函数的未定义的引用和全局变量(sleepsecs)替换为该进程的虚拟地址空间中机器代码所在的地址。

图4-6 EFL重定位信息

  1. 符号表

符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。

图4-7 EFL符号表信息

4.4 Hello.o的结果解析

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

图4-8 获得反汇编代码

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

图4-9 hello.s 中main函数以及反汇编后得到的main函数

通过对比hello.asm与hello.s可知,两者在如下地方存在差异:

  1. 分支转移:

在hello.s中,跳转指令的目标地址直接记为段名称,如.L2,.L3等。而在反汇编得到的hello.asm中,跳转的目标为具体的地址,在机器代码中体现为目标指令地址与当前指令下一条指令的地址之差。

图4-10 分支转移

2.函数调用:

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

图4-11 函数调用

3.全局变量访问

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

图4-12 全局变量访问

4.5 本章小结

本章对hello.s的结果进行了阐述,分析了可重定位文件的ELF头、节头部表、

符号表和可重定位节,比较了hello.s和hello.o反汇编代码的区别,分析了机器语

言的构成与汇编语言的映射关系。

5.1 链接的概念与作用

  1. 链接的概念

链接是通过链接器(Linker)将文件中调用的各种函数跟静态库及动态库链接,并将它们打包合并形成目标文件,即可执行文件。可执行文件可以被加载(复制)到内存并执行。

  1. 链接的作用

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

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.3 可执行目标文件hello的格式

   在Shell中输入命令 readelf -a hello > hello2.elf 生成 hello 程序的 ELF 格式文件,保存为hello2.elf(与第四章中的elf文件作区分):

图5-2 生成hello2.elf文件

打开hello2.elf,分析hello的ELF格式如下:

  1. ELF 头(ELF Header)

hello2.elf中的ELF头与hello.elf中的ELF头包含的信息种类基本相同,以 描述了生成该文件的系统的字的大小和字节顺序的16字节序列 Magic 开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。与hello.elf相比较,hello2.elf中的基本信息未发生改变(如Magic,类别等),而类型发生改变,程序头大小和节头数量增加,并且获得了入口地址。

图5-3 hello2.efl头信息

  1. 节头

hello2.elf中的节头包含了文件中出现的各个节的语义,包括节的类型、位置、偏移量和大小等信息。与hello.elf相比,其在链接之后的内容更加丰富详细(此处仅截取部分展示)。

图5-4 hello2.efl节头信息

  1. 程序头

程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。

图5-5 hello2.efl程序头信息

  1. 动态磁盘

图5-6 动态磁盘信息

  1. 符号表

符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明(此处仅截取部分展示)。

图5-7 符号表信息

5.4 hello的虚拟地址空间

使用edb加载hello,查看进程的虚拟地址空间各段信息,如图所示。

图5-8 hello节头表与虚拟地址空间各段

5.5 链接的重定位过程分析

执行命令:objdump -d -r hello > hello.objdump 得到hello的反汇编文件hello2.asm。与第四章中生成的hello.o.asm文件进行比较,其不同之处如下:

  1. 链接后函数数量增加。链接后的反汇编文件hello2.asm中,多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。

图5-9 链接后的函数

  1. 函数调用指令call的参数发生变化。在链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完整的反汇编代码。

图5-10 call指令的函数

  1. 跳转指令参数发生变化。在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。

图5-11 跳转指令的函数

5.6 hello的执行流程

使用edb执行hello,查看从加载hello到_start,到call main,以及程序终止的所有过程。下表列出其调用的程序名称与各个程序地址。

程序名称

程序地址

ld-2.27.so!_dl_start

0x7fce:8cc38ea0

ld-2.27.so!_dl_init

0x7fce:8cc47630

hello!_start

0x400500

libc-2.27.so!__libc_start_main

0x7fce:8c867ab0

-libc-2.27.so!__cxa_atexit

标签: 各式连接器fce17

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

 锐单商城 - 一站式电子元器件采购平台  

 深圳锐单电子有限公司