HIT-ICS2024大作业

计算机科学与技术学院

20245

 

本文简述了Hello.c源程序的预处理、编译、汇编、链接、运行的主要过程,以及hello程序的进程管理、存储管理与I/O管理,通过hello.c这一程序的程序周期的描述,对程序的编译、加载、运行有个初步的了解。

关键词:预处理;编译;汇编;链接;加载;进程;I/O管理                           

 

第1章 概述................................................................................... - 4 -

1.1 Hello简介............................................................................ - 4 -

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

1.3 中间结果............................................................................... - 4 -

1.4 本章小结............................................................................... - 4 -

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

2.1 预处理的概念与作用........................................................... - 5 -

2.2在Ubuntu下预处理的命令................................................ - 5 -

2.3 Hello的预处理结果解析.................................................... - 5 -

2.4 本章小结............................................................................... - 5 -

第3章 编译................................................................................... - 6 -

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

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

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

3.4 本章小结............................................................................... - 6 -

第4章 汇编................................................................................... - 7 -

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

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

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

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

4.5 本章小结............................................................................... - 7 -

第5章 链接................................................................................... - 8 -

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

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

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

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

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

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

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

5.8 本章小结............................................................................... - 9 -

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

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

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

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

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

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

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

6.7本章小结.............................................................................. - 10 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结............................................................................ - 12 -

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

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

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

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

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

8.5本章小结.............................................................................. - 13 -

结论............................................................................................... - 14 -

附件............................................................................................... - 15 -

参考文献....................................................................................... - 16 -

1章 概述

1.1 Hello简介

P2P(From Program to Process)

Hello程序在Linux系统下通过使用vimCode:Blockssublime等类似Windows记事本的文档编辑器(或者是IDE)等应用程序编写代码程序所得到的。得到的程序名称叫做hell.c,为Program

通过编译器驱动程序调动预处理器(cpp)、编译器(cl)、汇编器(as)、链接器(ld)四个程序获得二进制的可执行文件hello

shell解析命令行参数,并初始化环境变量等内容,获取argcargcenvp,解析命令行参数发现该命令非内置命令,将其视为可执行文件,并以(学号)(姓名)作为argv[1]argv[2]的内容。加载和运行过程调用fork函数创建进程、execve函数运行函数,通过内存映射、分配空间等手段让hello拥有自己的空间和时间,与其他程序并发地运行。由于没有’&’等符号,于是程序不在后台运行,hello在前台运行。至此程序Program转换为进程Process,即P2P

图1.1hello程序编译过程

图1.2运行结果

O2O(From Zero-0 to Zero-0):

Linux加载器execve()将程序计数器置为程序入口点,也就是_start函数的地址。这个函数是在系统目标文件ctrl.o中定义的。_start函数调用系统启动函数__libc_start_main,该函数定义在libc.so,它初始化执行函数,调用用户层的main函数,处理main函数的返回值,并在需要的时候把控制返回给内核。

通过段式管理、页式管理,各存储器联动,CPU为执行文件hello分配时间周期,执行逻辑控制流,每条指令在流水线上取值、译码、执行、访存、写回、更新PC。内存管理单元和CPU处理器在执行过程中通过L1、L2、L3高速缓存(SRAM)和TLB(翻译后备缓冲器)、多级页表在物理内存中存取数据、指令,通过I/O系统输入输出。当程序运行结束时(例如通过键盘中断Ctrl+C手段),shell回收进程,释放hello的内存并且删除有关进程上下文。hello从无倒有再到0的过程就是From Zero-0 to Zero-0,即O2O。

1.2 环境与工具

硬件环境:

X64 CPU;Intel Core i7 12700H; 2.30GHz; 32G RAM;

软件环境:

Windows11,Ubuntu 18.04.1LTS

开发与调试工具:

GCC,EDB,Hexedit,Objdump,readelf, Code:Blocks

1.3 中间结果

hello.c:源程序

hello.i:预处理后的文本文件

hello.s:编译后汇编程序文本文件

hello.o:汇编后的可重定位目标程序(二进制文件)

hello1.txt:hello.o的反汇编文件

hello2.txt:hello的反汇编文件

hello1elf.txt:ELF格式下的hello.o

hello2elf.txt:ELF格式下的hello

hello:链接后的可执行目标文件

1.4 本章小结

本章简述了hello.c源程序的程序周期:P2P和O2O的过程。并且介绍实验的软硬件环境、开发和调试工具,以及中间所产生的相关文件。

2章 预处理

2.1 预处理的概念与作用

概念:

预处理(pre-treatment),在程序设计领域,一般是指程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor)对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理为特定的单位。

最常见的预处理是C语言和C++语言。ISO C和ISO C++都规定程序由源代码被翻译分为若干有序的阶段(phase),通常前几个阶段由预处理器实现。

预处理中会展开以#起始的行,试图解释为预处理指令(preprocessing directive) ,其中ISO C/C++要求支持的包括:

#if

#ifdef

#ifndef

#else

#elif

#endif(条件编译)

#define(宏定义)

#include(源文件包含)

#line(行控制)

#error(错误指令)

#pragma(和实现相关的杂注) 单独的#(空指令)

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

预处理器在UNIX传统中通常缩写为PP,在自动构建脚本中C预处理器被缩写为CPP的宏指代。为了不造成歧义,C++(cee-plus-plus) 经常并不是缩写为CPP,而改成CXX。

作用:

预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。如hello.c中的行:

#include <stdio.h>

该命令告诉预处理器读取系统头文件stdio.h的内容,并且把它直接插入程序文本中。预处理器还会替换程序起始位置的宏。结果就得到了另一个C程序,通常是以.i作为文件扩展名。

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i

图2.1预处理命令及生成文件

2.3 Hello的预处理结果解析

查看hello.i文件,如图2.2所示:
函数main代码体保持原样,而全局变量sleepsecs之前的头文件被替换,约为3000多行,即扩展了#include文件,并在新的文件中也存在扩展新的#include引用。

图2.2预处理文件main函数

观察扩展头文件的部分,发现有大量对结构的定义,诸如typedef、struct、enum等等,对外部变量的引用,诸如extern,对引用目录的标注,诸如”/user/include/st dlib.h”。由于程序本身就无define类型的语句,我们此时看不出来define语句在预处理的替换。如图2.3所示:

图2.3预处理文件结构定义

2.4 本章小结

本章介绍了预处理阶段的相关概念、定义、应用以及方法,通过具体的hello实例说明预处理过程中对头文件stdio.h的解析、对头文件stdlib.h的解析、对头文件unistd.h的解析。浴池里过层中进行了头文件引用,define宏替换,删除注释。

3章 编译

3.1 编译的概念与作用

概念:

编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。编译的过程实质上是把预处理文件进行词法分析、语法分析、语义分析、优化,从C语言等高级语言转换为成机器更好理解的汇编语言程序,转换后的文件仍为ASCII文本文件。

作用:

编译后生成的.s汇编语言程序文本文件比预处理文件更容易让机器理解、比.o可重定位目标文件更容易让程序员理解,是对于程序像机器指令的一步关键过程。

3.2 在Ubuntu下编译的命令

gcc -S hello.c -o hello.s

图3.1编译命令及生成文件

3.3 Hello的编译结果解析

阅读生成的hello.s文件,对比源程序hello.c:

3.3.1 数据

①. 常量

C语言源程序如图3.2所示:

图3.2 C语言源程序

可以知道printf中的格式串为常量:

Usage: Hello 学号 姓名!\n

Hello %s %s\n

在汇编文本文件中,其被定义为.LC0,.LC1,如图所示,根据UTF-8的编码规则,汉字被编码为三个字节,其它英文、空格、换行等字符与ASCII规则相同,编码为一个字节,但汇编文件保留原字符形式。两字符串均存放在.rodata段中,如图3.3所示:

图3.3 printf格式串汇编代码

②. 变量

C语言源程序如图3.2所示,i、argc、argv为局部变量。

argc这一函数参数要求与5比较,在汇编程序中存放在寄存器EDI中,比较过程中放在栈中,帧指针RBP-20的位置。argv地址放在了寄存器RSI中,使用时放在栈中,帧指针RBP-32的过程。如图3.4所示:

图3.4 argc、argv条目

变量i作为循环计数器,先初始化为0,然后迭代每次与10比较判断循环是否结束,如图3.5所示:

图3.5 i条目

3.3.2 赋值

如图3.2所示,对变量的赋值 ‘=’:

对i的赋值,在循环for语句中

i=0 i++(等价于i=i+1)

图3.6 i的初始化

如图3.6所示,使用movl语句对i进行初始化。

3.3.3 算数操作

对i进行了i++的运算操作,如图3.7所示,使用了addl语句:

图3.7 i++操作

3.3.4 关系操作

程序进行了两次关系操作:

  • . argc!=5

使用了cmpl语句设置条件码,jxx语句根据条件码做出相应的是否跳转的选择,如图3.8所示:

图3.8 argc!=5

  • . i<10

如图3.9所示,每次进行i++后与9进行比较cmpl语句,若<=继续进行迭代jle语句:

图3.19 i<10

3.3.5 数组操作

如图3.2所示,数组相关操作只在printf格式串中使用argv[1]、argv[2]、argv[3],如图3.5所示,argv地址放在了寄存器RSI中,使用时放在栈中,帧指针RBP-32的过程。当argc==5是在循环中引用了argv数组内容。如图3.11所示,对argv的引用:

图3.10 数组argv操作

argv[0]地址从栈使用movl语句传递给rax,计算argv[2]地址,使用addq+16,加后的值是argv[2]地址,对其引用,argv[2]值存放在RDX,同理argv[1]、arg[3]的值存放在RSI、RCX中,存放在RDX,RSI,RCX中是方便printf的引用。

3.3.6 控制转移

如图3.2所示,控制转移出现在if(argc!=5)和for(i=0;i<10;i++)

  • . if(argc!=3)控制转移

如图3.12所示,argc存在栈-20(%rbp)中与5比较,若是不相等继续顺序执行,调用exit;相等则跳转至.L2执行for循环。这一过程主要通过je进行:

图3.11 if(argc!=3)控制转移

  • . for(i=0;i<10;i++)控制转移

如图3.13所示,for循环在.L2初始化,迭代结束后每次判断在.L3所示部分通过cmpl比较,jle比较,若i<=9跳转至.L4执行循环中的迭代,否则继续顺序执行printf、sleep函数:

图3.12 for循环控制转移

3.3.7 函数操作

(1). if(argc!=5)中对printf函数以及exit函数的调用

如图3.14所示。函数首先调用printf函数,将.rodata节的.LC0的printf格式串的地址存进EDI,作为第一个参数方便函数printf调用;再将1存进EDI作为exit函数的第一参数以供调用:

图3.13 if(argc!=3)的函数操作

(2). for循环中对printf、sleep的函数调用

如图3.12,函数将argv[1]、argv[2]、arf[3]的地址存进RSI、RDX、RCX,将printf格式串.LC1地址存进EDI,调用printf函数;再将arg[3]存进RDI,调用sleep函数。

(3). 对函数getchar的调用

如图3.14,函数直接调用getchar函数,调用后将存储main函数返回值的RAX寄存器置0,并返回结束main函数:

图3.14 getchar调用

3.4 本章小结

本节介绍编译器通过编译将.i文件转换为汇编语言的.s文件的过程。同时解析了变量,相关运算,以及各类c语言的基本语句的汇编表示,更容易理解高级语言的底层表示方法。

4章 汇编

4.1 汇编的概念与作用

概念:

汇编器(as)将hello.s文件翻译成二进制机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存到目标文件hello.o中。hello.o是一个二进制文件,包含着程序的指令编码,如果用文本编辑器打开,将看到一堆乱码。

作用:

汇编过程将汇编代码转换为计算机能够理解并执行的二进制机器代码,这个二进制机器代码是程序在本机器上的机器语言的表示。

4.2 在Ubuntu下汇编的命令

gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o

图4.1汇编命令及生成文件

4.3 可重定位目标elf格式

首先使用readelf命令查看hello.o的ELF格式,指令如下:

Readelf -a hello.o > hello1elf.txt

图4.2hello.o的elf格式的查看

4.3.1 ELF

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

如图4.3所示,hello.o相关信息如下:

图4.3 hello.o的ELF头信息

hello.o的ELF以一个16进制序列:

7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

作为ELF头的开头。这个序列描述了生成该文件的系统的字的大小为8字节和字节顺序为小端序。

ELF头的大小为64字节,目标文件的类型为REL(可重定位文件)、机器类型为Advanced Micro Devices X86-64即AMD X86-64、节头部表的文件偏移为0,以及节头部表中条目的大小,其数量为13。

4.3.2 节头部表(section header table)

如图4.4所示,节头部表描述了13个节的相关信息,由表可以看出:

.text节:以编译的机器代码,类型为PROGBITS,意为程序数据,旗标为AX,即权限为分配内存、可执行

.rela.text节:一个.text节中的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

.data节:已初始化的静态和全局C变量。类型为PROGBITS,意为程序数据,旗标为WA,即权限为可分配可写。

.bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。类型为NOBITS,意为暂时没有存储空间,旗标为WA,即权限为可分配可写。

.rodata节:存放只读数据,例如printf中的格式串和开关语句中的跳转表。类型为PROGBITS,意为程序数据,旗标为A,即权限为可分配。

.comment节:包含版本控制信息。 .note.GNU_stack节:用来标记executable stack(可执行堆栈)。

.eh_frame节:处理异常。 .rela.eh_frame节:.eh_frame的重定位信息。

.shstrtab节:该区域包含节区名称。 .symtab节:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。

.strtab节:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部的节名字。

图4.4 节头部表相关信息

4.3.3 符号表

一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。符号表有汇编器构造,使用编译器输出到汇编语言.s文件中的符号。每个符号表是一个条目的数组,每个条目包含如图4.5所示的部分:

图4.5 ELF符号表条目。type和binding字段每个都是4位

如图4.6所示,hello.o程序的符号表包含编号Num、Value、Size、Type、Bind、Vis、Ndx、Name字段:

图4.6 hello.o符号表

该符号表供16个符号,其中

sleepsecs是一个位于.data节(Ndx=3)偏移量(value)为0,大小为4个字节的全局符号,类型为变量。

main是一个位于.text节(Ndx=1)偏移量(value)为0,大小为125个字节的全局符号,类型为变量。

puts、exit、printf、sleep、getchar为NOTYPE未知类型,未定义(UND)符号。hello.c为文件,ABS表示不该被重定位的符号。

4.3.4 .rela.text节和.rela.eh_frame

如图4.7所示重定位条目包含以下信息:

图4.7 重定位条目信息

重定位条目常见共2种:

R_X86_64_32:重定位绝对引用。重定位时使用一个32位的绝对地址的引用,通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。

R_X86_64_PC32:重定位PC相对引用。重定位时使用一个32位PC相对地址的引用。一个PC相对地址就是据程序计数器的当前运行值的偏移量。

如图4.8所示,第一个、第四个重定位条目为绝对引用,其余均为相对PC引用。

重定位PC相对引用重定位算法如下:

refaddr = ADDR(s) + r.offset;

*refptr = (unsigned) (ADDR(r.symbol) + r.addend – refaddr); 重定位绝对引用重定位算法如下:

*refptr = (unsigned) (ADDR(r.symbol) + r.addend);

假设算法运行时,链接器为每个节(用ADDR(s)表示)和每个符号都选择了运行时地址(用ADDR(r.symbol))表示。

以.rodata的重定位为例,它的重定位地址为refptr.则应先计算引用的运行时地址refaddr=ADDR(s)+ r.offset, .rodata的offset为0x16,ADDR(s)是由链接器确定的。然后,更新引用,refptr = (unsigned) (ADDR(r.symbol) + r.addend – refaddr), .rodata的ADDR(r.symbol)也是由链接器确定的,addend查表可知为0,refaddr已经算出来了,所以,.rodata的重定位地址我们就可以算出来了。

图4.8 .rela.text节、.rela.eh_frame节信息

需要注意的是计算结果是unsingned类型。

4.4 Hello.o的结果解析

4.4.1命令

shell中输入 objdump -d -r hello.o > hello.asm 指令输出hello.o的反汇编文件,并与第3章的hello.s文件进行对照分析。

4.4.2与hel1o.s的对照分析

(1)增加机器语言

每一条指令增加了一个十六进制的表示,即该指令的机器语言。例如,在hello.s中的一个cmpl指令表示为

而在反汇编文件中表示为

(2)操作数进制

反汇编文件中的所有操作数都改为十六进制。如立即数由hello.s中的$5变为了$0x5,地址表示也由-20(%rbp)变为-0x14(%rbp)。可见只是进制表示改变,数值未发生改变。

(3)分支转移

反汇编的跳转指令中,所有跳转的位置被表示为主函数+段内偏移量这样确定的地址,而不再是段名称(例如.L2)。例如下面的jmp指令,反汇编文件中为

而hello.s文件中为

(4)函数调用

反汇编文件中对函数的调用与重定位条目相对应。观察下面call指令调用函数,在hello.s中为

而在反汇编文件中调用函数为

在可重定位文件中call后面不再是函数名称,而是一条重定位条目指引的信息。

4.5 本章小结

这一章介绍了汇编的含义和功能。以Ubuntu系统下的hello.s文件为例,说明了如何把它汇编成hello.o文件,并生成ELF格式的可执行文件hello.elf。将可重定位目标文件改为ELF格式观察文件内容,对文件中的每个节进行简单解析。通过分析hello.o的反汇编代码(保存在hello.asm中)和hello.s的区别和相同点,让人清楚地理解了汇编语言到机器语言的转换过程,以及机器为了链接而做的准备工作。

5章 链接

5.1 链接的概念与作用

5.1.1链接的概念

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

5.1.2链接的作用

在现代系统中,链接是由叫做链接器(1iker)的程序自动执行的,它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用。

5.2 在Ubuntu下链接的命令

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

    使用readelf解析helloELF格式,得到hello的节信息和段信息:

1ELF头(ELF Header

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

2)节头

描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。

3)程序头

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

4Dynamic section

5Symbol table

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

5.4 hello的虚拟地址空间

    观察程序头的LOAD可加载的程序段的地址为0x400000。如图:

使用edb打开hello从Data Dump窗口观察hello加载到虚拟地址的情况,查

看各段信息。如图:

程序从地址0x400000开始到0x401000被载入,虚拟地址从0x4000000x400f0结束,根据5.3中的节头部表,可以通过edb找到各段的信息。

如.interp节,在hello.elf文件中能看到开始的虚拟地址:

在edb中找到对应的信息:

同样的,我们可以找到如.text节的信息:

5.5 链接的重定位过程分析

5.5.1分析hello与hello.o区别

Shell中使用命令objdump -d -r hello > hello1.asm生成反汇编文件hello1.asm

与第四章中生成的hello.asm文件进行比较,其不同之处如下:

(1)链接后函数数量增加

链接后的反汇编文件hello1.asm中,多出了.pltputs@pltprintf@pltgetchar@pltexit@pltsleep@plt等函数的代码。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。

(2)函数调用指令call的参数发生变化

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

(3)跳转指令参数发生变化

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

5.5.2重定位过程

重定位由两步组成:

(1)重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的聚合节。然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。至此程序中每条指令和全局变量都有唯一的运行内存地址。

(2)重定位节中的符号引用。这一步中链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。

重定位过程地址计算方法如下:

5.6 hello的执行流程

5.6.1过程

通过edb的调试,一步一步地记录下call命令进入的函数。

(I)开始执行:_start、_libe_start_main

(2)执行main:_main、printf、_exit、_sleep、getchar

(3)退出:exit

5.6.2子程序名或地址

程序名               程序地址

_start                0x400550

_libc_start_main       0x7fd1f58aaba0

main                0x400582

_printf               0x7fd1f59b2a30

_sleep               0x7fd1f596d520

getchar             0x7fd1f5910ea0

_exit                0x7d1f596da80

5.7 Hello的动态链接分析

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。延迟绑定是通过GOT和PLT实现的,根据hello.elf文件可知,GOT起始表位置为:0x601000:

GOT表位置在调用dl_init之前0x601000后的16个字节均为0;

调用了dl_init之后字节改变了:

对于变量而言,利用代码段和数据段的相对位置不变的原则去计算正确地址。

对于库函数而言,需要pltgot合作。plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。接下来执行程序的过程中,就可以使用过程链接表plt和全局偏移量表got进行动态链接。

5.8 本章小结

本章首先阐述了链接的基本概念和作用,展示了使用命令链接生成hello可执行文件,观察了hello文件ELF格式下的内容,利用edb观察了hello文件的虚拟地址空间使用情况,最后以hello程序为例对重定位过程、执行过程和动态链接进行分析。

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1进程的概念

进程的经典定义就是一个执行中程序的实例。进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

6.1.2进程的作用

进程为程序提供了一种假象,程序好像是独占的使用处理器和内存,处理器好像是无间断地一条接一条地执行我们程序中的指令。进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中。

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

6.2.1 Shell-bash的作用

Shell是一个交互型应用级程序,也被称为命令解析器,它为用户提供一个操作界面,接受用户输入的命令,并调度相应的应用程序。

6.2.2 Shell-bash的处理流程

首先从终端读入输入的命令,对输入的命令进行解析,如果该命令为内置命令,则立即执行命令,否则调用fork创建一个新的子进程,在该子进程的上下文中执行指定的程序。判断该程序为前台程序还是后台程序,如果为前台程序则等待程序执行结束,若为后台程序则将其放回后台并返回。在过程中shell可以接受从键盘输入的信号并对其进行处理。

6.3 Hello的fork进程创建过程

首先用户再shel1界面输入指令:./hel1o 2022112666 苏智武 13694966146 1

Shell判断该指令不是内置命令,于是父进程调用fork函数创建一个新的子进程,该子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程与父进程最大的区别就是具有不同的PID。在父进程中,fork返回子进程的PID,而在子进程中fork返回0,返回值提供一个明确的方法来分辨程序是父进程还是在子进程中执行。

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个程序。函数声明如下:

int execve(const char *filename, const char *argv[], const char *envp[]);

execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并不返回。main函数运行时,用户栈的结构如图所示:

6.5 Hello的进程执行

hello程序在运行时,进程提供给应用程序的抽象有:(1)一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占地使用处理器;(2)一个私有的地址空问,它提供一个假象,好像我们的程序独占地使用CPU内存。

操作系统提供的抽象有:

(1)逻辑控制流。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。

(2)上下文切换。操作系统内核使用一种称为上下文切换的叫高层形式的异常控制流来实现多任务。内核为每一个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需状态。

(3)时间片。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。

(4)用户模式和内核模式。处理器通常使用某个控制寄存器中的一个模式位来提供这种功能。当设置了模式位时,进程就运行在内核模式里。一个运行在内核模式的进程可以执行指令集中的所有指令且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,也不能直接引用地址空间中内核区内的代码和数据。

(5)上下文信息。上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。hello程序执行过程中,在进程调用execve函数后,进程就为hello程序分配新的虚拟地址空间,开始时程序运行在用户模式中,调用printf函数输出“2022112666 苏智武 13694966146”,之后调用sleep函数,进程进入内核模式,运行信号处理程序,再返回用户模式,运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。

6.6 hello的异常与信号处理

6.6.1异常的分类

6.6.2异常的处理方式

6.6.3运行结果及相关命令

1)正常运行状态

在程序正常运行时,打印10次提示信息,以输入回车为标志结束程序,并回收进程。

2)运行时按下Ctrl + C

按下Ctrl + CShell进程收到SIGINT信号,Shell结束并回收hello进程。

3)运行时按下Ctrl + Z

按下Ctrl + ZShell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。

(4)hello进程的挂起可由psjobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1

5Shell中输入pstree命令,可以将所有进程以树状图显示:

(6)输入kill命令,则可以杀死指定(进程组的)进程:

(7)

输入fg 1则命令将hello进程再次调到前台执行,可以发现Shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下的语句。程序仍然可以正常结束,并完成进程回收。

8)不停乱按

在程序执行过程中乱按所造成的输入均缓存到stdin,当getchar的时候读出一个’\n’结尾的字串(作为一次输入),hello结束后,stdin中的其他字串会当做Shell的命令行输入。

6.7本章小结

本章的主要内容是探讨计算机系统中的进程和shell,首先通过一个简单的hello程序,简要介绍了进程的概念和作用、shell的作用和处理流程,还详细分析了hello程序的进程创建、启动和执行过程,最后,本章对hello程序可能出现的异常情况,以及运行结果中的各种输入进行了解释和说明。

第7章 hello的存储管理

7.1 hello的存储器地址空间

7.1.1逻辑地址

在有地址变换功能的计算机中,访问指令给出的地址(操作数)叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。逻辑地址是由一个段标识符加上一个指定段内相对地址的偏移量,由程序hello产生的与段相关的偏移地址部分

7.1.2线性地址

线性地址是逻辑地址到物理地址变换之间的一步,程序hello的代码会产生逻辑地址,在分段部件中逻辑地址是段中的偏移地址,加上基地址就是线性地址。

7.1.3虚拟地址

程序访问存储器所使用的逻辑地址称为虚拟地址。虚拟地址经过地址翻译得到物理地址。与实际物理内存容量无关,是hello中的虚拟地址

7.1.4物理地址

在存储器里以字节为单位存储信息,每一个字节单元给一个唯一的存储器地址,这个地址称为物理地址,是hello的实际地址或绝对地址。

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

段式管理是指把一个程序分成若干个段进行存储,每个段都是一个逻辑实体。段式管理是通过段表进行的,包括段号(段名)、段起点、装入位、段的长度等。程序通过分段划分为多个块,如代码段、数据段、共享段等。

一个逻辑地址是两部分组成的,包括段标识符和段内偏移量。段标识符是由一个16位长的字段组成的,称为段选择符。其中前13位是一个索引号,后3位为一些硬件细节。索引号即是“段描述符”的索引,段描述符具体地址描述了一个段,很多个段描述符就组成了段描述符表。通过段标识符的前13位直接在段描述符表中找到一个具体的段描述符。

全局描述符表(GDT)整个系统只有一个,它包含:(1)操作系统使用的代码段、数据段、堆栈段的描述符(2)各任务、程序的LDT(局部描述符表)段。

每个任务程序有一个独立的LDT,包含:(1)对应任务/程序私有的代码段、数据段、堆栈段的描述符(2)对应任务/程序使用的门描述符:任务门、调用门等。

段式管理图示如下:

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

虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。VM系统将虚拟内存分割,称为虚拟页,类似地,物理内存也被分割成物理页。利用页表来管理虚拟页,页表就是一个页表条目(PTE)的数组,每个PTE由一个有效位和一个位地址字段组成,有效位表明了该虚拟页当前是否被缓存在DRAM中,如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,如果发生缺页,则从磁盘读取。

MMU利用页表来实现从虚拟地址到物理地址的翻译。

下面为页式管理的图示:

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

Core i7采用四级页表的层次结构。CPU产生虚拟地址VA,虚拟地址VA传送给MU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。如果命中,则得到物理地址PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。工作原理如下:

多级页表的工作原理展示如下:

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

如图为高速缓存存储器组织结构:

高速缓存的结构将m个地址位划分成了t个标记位,s个组索引位和b个块偏移位:

如果选中的组存在一行有效位为1,且标记位与地址中的标记位相匹配,我们就得到了一个缓存命中,否则就称为缓存不命中。如果缓存不命中,那么它需要从存储器层次结构的下一层中取出被请求的块,然后将新的块存储在组索引位指示组中的一个高速缓存行中,具体替换哪一行取决于替换策略,例如LRU策略会替换最后一次访问时间最久远的那一行。

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_ struct、.区域结构和页表的原样副本。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个。后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。

(2)映射私有区域。为新程序的代码、数据、.bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,.bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。

(3)映射共享区域。hello程序与共享对象1ibc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

(4)设置程序计数器。execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。如图所示:

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

如果程序执行过程中发生了缺页故障,则内核调用缺页处理程序。处理程序执行如下步骤:

(1)检查虚拟地址是否合法,如果不合法则触发一个段错误,终止这个进程。

(2)检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。

(3)两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。

7.9本章小结

本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core i7在指定环境下介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射、hello进程、execve时的内存映射、缺页故障与缺页中断处理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。

设备的模型化:文件

设备管理:unix io接口

8. 2 简述Unix IO接口及其函数

Unix I/O接口统一操作:

设备可以通过Unix I/O接口被映射为文件,这使得所有的输入和输出都能以一种统一且一致的方式来执行:

打开文件。 一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

Linux shell创建的每个进程开始时都有三个打开的文件: 标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可以用来代替显式的描述符值。

改变当前的文件位置。 对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。

读写文件。 一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的而文件,当k>=m时执行读操作会触发一个成为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

关闭文件。 当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

Unix I/O接口函数:

①. 进程是通过调用open函数来打开一个存在的文件或者创建一个新文件的,函数声明如下:

        int open(char *filename, int flags, mode_t mode);

open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件。mode参数指定了新文件的访问权限位。作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置成mode&~umask。

②. 进程通过调用close函数关闭一个打开的文件。函数声明如下:

int close(int fd);

        成功返回0错误返回EOF

③. 应用程序是通过分别调用read和write函数来执行输入和输出的。函数声明如下:

        ssize_t read(int fd, void *buf, size_t n);

        ssize_t write(int fd, const void *buf, size_t n);

        read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

        ④. 通过调用lseek函数,应用程序能够显式地修改当前文件的位置。函数声明如下:

        off_t lseek(int handle, off_t offset, int fromwhere);

8. 3 printf的实现分析

int printf(const char *fmt, ...)

{

                     int i;

                     char buf[256];

                     va_list arg = (va_list)((char*)(&fmt) + 4);

                     i = vsprintf(buf, fmt, arg);

                     write(buf, i);

                 return i;

}

printf需要做的事情是:接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。printf用了两个外部函数,一个是vsprintf,还有一个是write。

vsprintf函数作用是接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

        write函数将buf中的i个元素写到终端。

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8. 4 getchar的实现分析

int getchar(void)

{

                     static char buf[BUFSIZ];//缓冲区

                     static char* bb=buf;//指向缓冲区的第一个位置的指针

                     static int n=0;//静态变量记录个数

                     if(n==0)

                     {

                            n=read(0,buf,BUFSIZ);

                            bb=buf;//并且指向它

                     }

                     return(--n>=0)?(unsigned char)*bb++:EOF;

}

getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。

当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8. 5本章小结

本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。

结论

hello所经历的过程:

首先由程序员将hello代码从键盘输入,依次要经过以下步骤:

1、预处理(cpp)。将hello.c进行预处理,将文件调用的所有外部库文件合并展开,生成一个经过修改的hello.i文件。

2、编译(ccl)。将hello.i文件翻译成为一个包含汇编语言的文件hello.s。

3、汇编(as)。将hello.s翻译成为一个可重定位目标文件hello.o。

4、链接(ld)。将hello.o文件和可重定位目标文件和动态链接库链接起来,生成一个可执行目标文件hello。

5、运行。在shel1中输入./hello 2022112666 苏智武 13694966146 1。

6、创建进程。终端判断输入的指令不是shell内置指令,于是调用fork函数创建一个新的子进程。

7、加载程序。shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。

8、执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。

9、访问内存:MU将程序中使用的虚拟内存地址通过页表映射成物理地址。

10、信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。

11、终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。

感悟:

通过本次实验,我深切感受到计算机系统的精细和强大,每一个简单的任务都需要计算机的各种复杂的操作来完成,这背后体现出了严谨的逻辑和现代工艺的精巧。

附件

文件名

功能

hello.c

源程序

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello.elf

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

hello.asm

反汇编hello.o得到的反汇编文件

hello1.asm

反汇编hello可执行文件得到的反汇编文件

hello

可执行文件

参考文献

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值