ICS大作业论文

编程入门 行业动态 更新时间:2024-10-24 18:16:42

计算机系统

作业

题     目  程序人生-Hellos P2P  

专       业       计算机类         

学     号      120L020430        

班   级       2003004          

学       生        裴建平          

指 导 教 师        史先俊            

计算机科学与技术学院

2022年5月

摘  要

本文主要探究了hello程序的一生。通过一系列的开发工具从最开始的hello.c文件进行预处理、汇编、编译、链接最终得到可执行文件,并且根据每一步的变化情况,来分析hello是如何产生的。同时分析了从创建该程序的进程开始,进行加载然后运行到最后被回收的整个过程。对于此过程中进程会遇到的信号与异常、进程的存储管理、与I/O的交互都一一进行了介绍。由此,我们能够更加深入地理解计算机系统是怎样工作的。

关键词:hello;预处理;汇编;编译;链接;进程;信号;存储;I/O;                            

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.0 伪指令

3.3.1 数据

3.3.2 赋值

3.3.3 类型转换

3.3.4 算数操作

3.3.5 关系操作

3.3.6 控制转移

3.3.7 函数操作

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.3.1 ELF Header

4.3.2 节头部表

4.3.3 .symtab

4.3.4 .rel.text

4.4 Hello.o的结果解析

4.5 本章小结

5链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.3.1 ELF Header

5.3.2 节头部表

5.3.3 .symtab

5.3.4 .rel.text

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.5.1 链接的过程

5.5.2 重定位的过程

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

6hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

7hello的存储管理

7.1 hello的存储器地址空间

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本章小结

8hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

P2P:Hello程序的生命周期是从一个高级C语言程序开始的,也就是hello.c。在linux系统上,随后需要经过编译器驱动程序转化为可执行目标文件。这其中,首先要被预处理变为hello.i源程序,接着被编译为hello.s汇编程序,再经过汇编器的汇编生成hello.o这样的可重定位目标程序,接下来与共享目标文件被链接器链接形成了可执行目标文件。在shell中键入启动命令后,调用fork函数为该程序创建子进程,这样程序就变成了进程。

020:接下来,shell调用execve函数,通过把这个进程映射虚拟内存,程序加载到物理内存当中,创建上下文,在main函数中执行相关代码,此时也需要CPU为其分配时间片执行逻辑控制流。在运行结束后,由父进程回收该子进程,内核将其从内存中删除。

1.2 环境与工具

软件环境:Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上;

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上

开发与调试工具:Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc;edb;readelf;objdump

1.3 中间结果

文件名称

文件作用

hello.i

对hello.c预处理之后生成的文件。

hello.s

对hello.i编译生成的汇编语言文件。

hello.o

对hello.s汇编生成的可重定位目标文件

hello

对hello.o链接之后生成的可执行文件。

hello.oelf.txt

hello.o的elf格式的文本文件

hello.odump.txt

对hello.o进行反汇编生成的文本文件

helloelf.txt

hello的elf格式的文本文件

hellodump.txt

对hello进行反汇编生成的文本文件

1.4 本章小结

本章主要简单介绍了hello的P2P、020的整个过程,列出了在这次实验中需要用到的软硬件环境、用到的开发工具以及本次大作业产生的所有中间过程文件。


第2章 预处理

2.1 预处理的概念与作用

预处理概念:预处理是指预处理器cpp根据以字符#开头的命令,修改原始的C程序,形成完整的C程序,通常是以.i作为文件拓展名。

预处理作用:

  • 处理宏定义指令。把宏名替换为其所定义的字符串或者参数,为下一步编译做准备;
  • 处理文件包含指令。把所有该程序需要用到的头文件的内容与该程序进行合并,形成新的源程序;
  • 条件编译。按照#ifdef等指令进行修改,使得在下一步编译过程中,有些语句只在条件满足才执行。

2.2在Ubuntu下预处理的命令

命令:gcc hello.c -E -o hello.i,如图2-1.

图2-1 预处理的指令截图

2.3 Hello的预处理结果解析

图2-2 预处理结果截图

从图2-2可知,预处理之后新生成了hello.i文件,这仍然是一个C代码文件,但在原来的基础上,加入了相关库文件中的内容,同时把定义的宏名替换为了其所定义的字符串。

2.4 本章小结

本章介绍了如何对hello.c进行预处理,以及预处理的结果,例如导入头文件的内容等等,为之后的编译做好铺垫。


第3章 编译

3.1 编译的概念与作用

编译的概念:编译器ccl将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。

编译的作用:把每条C语言语句都翻译为文本格式的低级机器语言指令,为不同高级语言的不同编译器提供了通用的输出语言。如果发现语法错误会进行提示。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s,如图3-1.

图3-1 编译命令截图

3.3 Hello的编译结果解析

3.3.0 伪指令

如图3-2所示:

.file:声明源文件

.text:代码节

.section .rodata:只读数据

.align:地址对齐方式

.string:声明字符串

.global:声明全局变量

.type:声明数据类型,这里main就代表函数

图3-2 hello.s中头部伪指令

3.3.1 数据

  • 字符串

编译器把printf中需要打印的两个语句声明为两个字符串,全英文的语句会保存为英文,有特殊字符的会将其转换为它的编码形式,如图3-3。

图3-3 hello.s中字符串声明

  • 参数

把传入到main函数的参数都从寄存器%rdi和%rsi中放入到了堆栈中,如图3-4。

图3-4 hello.s中参数的保存与使用

  • 常量

把原来的常数,直接作为立即数使用,如图3-5。

图3-5 hello.s中常量4的使用

  • 局部变量

把用作循环变量的i直接放入到了堆栈中,如图3-6。

图3-6 hello.s中局部变量i的保存与使用

  • 数组

把源程序数组的起始位置放在了寄存器%rax中,如图3-7。通过增加偏移量来更改寄存器中保存地址使其指向argv[1], argv[2]等元素,如图3-8。

图3-7 hello.s中数组argv的保存

图3-8 hello.s中访问数组argv中的元素值

3.3.2 赋值

使用movl函数把循环变量赋初值为0,并保存在堆栈中,如图3-9。

图3-9 循环变量赋初值

3.3.3 类型转换

直接调用atoi函数,把字符串类型转换为int类型,如图3-10。

图3-10 使用atoi进行类型转换

3.3.4 算数操作

在hello.c中唯一的算数操作就是循环变量的自增运算,使用addl把堆栈中的i加1,再放回堆栈当中,如图3-11。

图3-11 加法运算操作

3.3.5 关系操作

源程序中共有两处进行了关系操作。第一处使用cmpl比较argc和4是否相等,如果相等就跳转到.L2,(不执行if结构体中的语句),如图3-12. 第二处是判断循环是否结束,把循环变量与7进行比较,如果不相等就执行循环体中的语句,如图3-13.

图3-12 比较argc和4是否相等

图3-13 判断循环是否结束

3.3.6 控制转移

如上图3-12所示表示的就是if (argc!=4)语句,如果不相等,接着向下执行,否则跳转到.L2。上图3-13则是表示语句for(i=0;i<8;i++),满足循环条件就跳转到.L4,否则向下执行。从中可以看出jmp系列指令类似c语言中的goto语句。

3.3.7 函数操作

在执行函数操作时,程序会把这个程序要用的参数放在%rdi, %rsi等寄存器中,如果寄存器不能放下全部参数还会把一部分参数放入堆栈中。然后执行call指令,把当前指令地址放入堆栈中,程序计数器指向函数地址,执行完函数之后,把返回值放入%rax寄存器中,释放用到的空间再弹栈接着执行下一条指令。如图分别是该程序调用puts, exit, printf, atoi, sleep, getchar函数。

图3-14 调用puts函数

图3-15 调用exit函数

图3-16 调用printf函数

图3-17 调用atoi函数

图3-18 调用sleep函数

图3-19 调用getchar函数

3.4 本章小结

本章就要讲述了编译器是如何进行编译的,并对编译的结果进行解析。hello.s中的汇编语言程序语句能够与原来的c语言程序语句一一对应。


第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编器as将hello.s翻译成机器语言指令,用这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件。

汇编的作用:形成了机器能够“看得懂”的语言,距离执行越来越近了!

4.2 在Ubuntu下汇编的命令

gcc hello.s -c -o hello.o,如图4-1所示。

图4-1 执行汇编指令

4.3 可重定位目标elf格式

4.3.1 ELF Header

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

图4-2 ELF Header

4.3.2 节头部表

如图4-3使用指令readelf -S hello.o获取节头部表。这里保存着各个节的信息,包括名称、大小、类型、地址等。由于是可重定位的,所以目前的地址都是0,用于重定位。根据下面的flags的提示信息可知,哪些是只读的,哪些是可写的。利用节头表的偏移和各个节的偏移量可以得到各个节在文件中的位置。

图4-3 节头部表

4.3.3 .symtab

使用指令readelf -s hello.o可以查看符号表中的信息,如图4-4。其中存放了在程序中定义和引用的函数和全局变量的信息,包括值,大小,种类,作用范围等等。例如main函数的值是0,大小为146字节,种类是FUNC也就是函数,GLOBAL表示全局变量。

图4-4 .symtab

4.3.4 .rel.text

使用指令readelf -r hello.o指令可以查看程序中可重定位段的信息,如图4-5。这里保存的是.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。表中包括偏移量、信息(前四个字节表示应该指向的符号后四个字节表示重定位的类型)、类型(告知链接器如何进行重定位)、符号值和符号名称以及加数。

图4-5 .rel.text

4.4 Hello.o的结果解析

使用指令objdump -d -r hello.o对hello.o进行反汇编,得到了图4-6中的结果,接下来将其与hello.s(图4-7)中的内容进行对比。

通过对比,我们可以发现两者的结果几乎没有不同,只不过反汇编后多了机器代码的显示,同时立即数采用十六进制表示,而不是十进制。机器语言是用二进制数表示的语言,是电脑能够“读”的语言,机器指令由操作码和操作数构成。汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。每一条汇编语言操作码都可以用机器二进制数来表示,进而可以将所有的汇编语言和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言。两者结果的主要不同如下。

  • 分支转移:反汇编中的分支转移是跳转到相对main的偏移地址,在hello.s中是跳转到段,如.L3。如图4-6和4-7红色部分。原因在于段名称只是汇编语言中的助记符,在这里需要确切的地址。
  • 函数调用:在hello.s中函数调用call后面是函数的名称,而在反汇编的结果当中,call后面是下一条指令的地址,同时调用函数的重定位信息加到了.rel.text中,原因就在于,那些共享库中的函数,在链接阶段才会加入,此时没有确切的位置,需要重定位才可以。如图4-6和4-7蓝色部分。

图4-6 hello.o反汇编结果

图4-7 hello.s

4.5 本章小结

这一章主要描述的是把汇编语言程序汇编为机器语言二进制码的过程和结果,体现两种语言间的映射关系。通过readelf指令可以了解各个节中的信息;通过对生成的.o文件反汇编,与.s文件进行对比,能够更清晰地了解汇编做了什么。


5链接

5.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 /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o hello.o进行链接,如图5-1所示。

图5-1 链接

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

5.3.1 ELF Header

从图5-2可以看出,ELF Header的大小是64字节,节头部表的大小是64字节。与hello.o的不同之处用红色标记表示,首先是类型变为可执行文件了,其次在经过重定位之后,入口点地址,程序头起点,节头部表起点,程序和节头部表的大小和条目的数量都发生了改变。

5.3.2 节头部表

从图5-3的节头部表中,能够看出hello共有27个节,各个节的大小和地址都直接有所体现。

图5-2 hello的ELF Header

图5-3 hello的节头部表

5.3.3 .symtab

图5-4展示的是hello中的.symtab中的内容。

图5-4 hello的.symtab

5.3.4 .rel.text

图5-5展示的是.rel.text中的内容。

图5-5 hello中的.rel.text

5.4 hello的虚拟地址空间  

  • 图5-6所示为hello在edb中查看到的ELF Header对应的虚拟地址空间,可以看到从0x400000开始,前十六位的序列与图5-2中的magic序列一致,其他的信息则为十六进制表示。

图5-6 ELF Header的虚拟地址空间

  • 根据图5-3中节头表中的信息能够知道每个节所在的位置,例如.text在0x4010f0,可以用edb查看,如图5-7。

图5-7 .text在虚拟地址空间的表示

5.5 链接的重定位过程分析

使用命令objdump -d -r hello >hellodump.txt,得到了hello的反汇编结果,下面是对它的分析。

  • 首先能够看到相比于hello.o的反汇编结果中main的地址是0,其他指令都是关于main的相对地址,hello的反汇编结果中每条指令和函数有了具体的虚拟地址,如图5-8,5-9所示。

图5-8 hello反汇编结果中main的一部分

图5-9 hello.o反汇编结果中main的一部分

  • 除此之外,hello的反汇编代码中还多了调用的外部库中的很多节和函数的汇编代码,如图5-10。

图5-10 hello反汇编结果中新增的部分节与函数

5.5.1 链接的过程

由以上结果,我们可以得出结论链接首先要进行符号解析,把每个符号引用正好和一个符号定义关联起来,这就是新增的部分节的作用。下一步要进行重定位,获取所需外部库的内容之后,把每个符号定义与一个内存位置关联起来,从而重定位这些节,使得符号引用指向这个内存位置。

5.5.2 重定位的过程

  • 重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
  • 重定位节中的符号引用。在这一步中,链接器修改代码节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中成为重定位条目的数据结构。
  • 重定位条目就是图4-5中所展现的内容,它的定义如图5-11。从图4-6能够知道,在链接之前,重定位条目没有确切的地址,对其应用时会被加入到重定位条目中,下面给出重定位算法如图5-12。

图5-11 重定位条目的数据结构定义

图5-12 重定位算法

5.6 hello的执行流程

  • 从加载到_start:在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段。接下来加载器跳转到程序的入口点,也就是_start函数的地址。
  • 从_start到main:调用系统函数_libc_start_main,再调用_cxa_atexit,然后执行main。
  • 从main到程序终止:依次经过printf, atoi, sleep, getchar,在跳转之前都会先到达其对应的@plt函数,然后跳转到.plt,最后才会到达共享库中的相应函数,都执行完毕后,程序终止。

调用和跳转的子程序名和地址如表5-13所示。

_start

0x4010f0

libc-2.31.so!_libc_start_main

0x00007f9a:e2cfef90

libc-2.31.so!_cxa_atexit

0x00007f9a:e2d21de0

main

0x4011a5

printf@plt

0x4010a0

.plt

0x401020

atoi@plt

0x4010c0

sleep@plt

0x4010e0

getchar@plt

0x4010b0

表5-13 子程序名和地址

5.7 Hello的动态链接分析

在第一次调用一个函数时,首先跳转到它所对应的PLT条目,执行命令,跳转到它的GOT条目指向的地址中,初始时指向PLT条目的下一条指令,然后会跳转到动态链接器,由动态链接器确定该函数的运行时位置,再把这个位置重写GOT条目,然后执行函数。这样再次执行这个函数时,就会跳转到PLT条目中然后直接跳转到对应GOT条目指向的函数地址。

由图5-3可知GOT条目位于0x404000,在调用di_init之前其中的内容如图5-14所示。现在这里0x404008之后十六个字节的内容都是空的。

图5-14 初始的GOT表

从_start运行到main后GOT中的内容如图5-15所示。现在可以看到0x404008之后十六个字节已经有了内容,0x7fe33dbbb190和0x7fe33dba4dc0。

图5-15 GOT表初始化后内容

以printf函数为例,如图5-16,将会跳转到0x401040处,而这个位置恰好是GOT条目中0x404028中保存的内容,由此可知这个就是printf对应的GOT条目。当第一次执行完printf函数之后的GOT条目表已经发生了变化,0x404028中保存的内容已经变为了0x7f8e235fec90(如图5-17),跳转到该地址后发现,此地址正是共享库中printf函数的入口(如图5-18),此后再次调用这个函数时,都会从PLT条目跳转到这里。

图5-16 printf函数

图5-17 printf第一次执行后的GOT条目

图5-18 共享库的printf地址

5.8 本章小结

本章主要讲述了链接的概念与作用以及如何进行链接,分析了链接前后的文件变化情况由此得出如何进行重定位的过程,最后分析了动态链接过程。到这里已经实现了将hello.c变为可执行文件的过程。


6hello进程管理

6.1 进程的概念与作用

进程的概念:进程的经典定义是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。由于进程的存在,我们会得到一个假象,好像我们的程序是系统中当前运行的唯一的程序一样。

进程的作用: 提供了一个独立的逻辑控制流和一个私有的地址空间,提供了好像我们的程序独占地使用处理器和内存系统的假象。

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

shell是一个交互性的应用级程序,它代表用户运行其他程序。

其基本功能是解释并运行用户的指令,重复如下处理过程:

  • 终端进程读取用户由键盘输入的命令行;
  • 分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量;
  • 检查第一个命令行参数是否是一个内置的shell命令;
  • 如果不是内部命令,调用fork( )创建子进程;
  • 在子进程中,用②获取的参数,调用execve( )执行指定程序;
  • 如果用户没要求后台运行(命令末尾没有&号),shell使用waitpid或wait等待作业终止后返回;
  • 如果用户要求后台运行(如果命令末尾有&号),则shell返回。

6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程获得与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和子进程之间最大的区别在于它们有不同的PID。

以hello为例,在键盘输入 ./hello 120L020430 裴建平 1 的时候,首先shell对其进行解析,由于它不是一个内置的shell命令,因此shell会调用fork()创建一个子进程。首先,内核为新进程创建各种数据结构,并为其分配一个唯一的PID。会复制当前进程的副本,并且把两个进程的每个页面度标记为只读,并将两个进程中的每个区域都标记为私有的写时复制。当fork从新进程中返回之后,新进程或者原来的进程在进行写操作时,就会创建新页面,是的每个进程保持了私有地址空间。

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新程序。利用loader,它加载并运行可执行目标文件且带参数列表和环境变量列表。加载并运行hello需要以下几个步骤:

  • 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
  • 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。如图6-1概括了私有区域的不同映射。
  • 映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
  • 设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

图6-1 加载器是如何映射用户地址空间的区域的

6.5 Hello的进程执行

hello进程的执行是依赖于进程所提供的抽象的基础上,下面阐述操作系统所提供的进程抽象:

  • 逻辑控制流:一系列的程序计数器(PC)的值的序列叫做逻辑控制流,或者简称逻辑流。进程是轮流使用处理器的。每个进程执行它的流的一部分,让毕业被抢占(暂时挂起),然后轮到其他进程。
  • 并发流:一个逻辑流的执行在时间上与另一个流重叠,成为并发流,这两个流称为并发地运行。多个流并发地执行的一般现象被称为并发。一个进程和其他进程轮流运行的概念称为多任务。
  • 时间片:一个进程执行它的逻辑控制流的一部分的每一时间段叫做时间片。因此多任务也叫作时间分片。
  • 私有地址空间:进程也为每个程序提供一种假象,好像它独占地使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,从这个意义上讲,这个地址空间是私有的。
  • 用户模式和内核模式:处理器提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理通常是用某个控制寄存器中的一个模式位来提供这种功能的,该寄存器描述了进程当前享有的的特权。当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式的进程不允许执行特权指令,也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。
  • 上下文信息:内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。
  • 上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。

下面来分析hello进程的执行。当内核调用fork和execve函数之后,此时he进程已经有了它自己的虚拟内存和虚拟地址空间,它的.text和.data节都已经被加载到虚拟内存的代码区和数据区。当程序计数器从_start开始进入到main之后,这个进程被正式执行,这时根据PC的值,进程执行相应地址的值对应的命令,这时是运行在用户态的。在输出Hello 120L020430 裴建平之后,hello调用sleep函数,进程进入内核模式,内核把执行上下文切换,运行另一个进程,当sleep函数执行结束之后,内核把当前进程挂起,再上下文切换到hello进程,随后hello程序接着执行自己的逻辑控制流,运行示例如图6-2。

图6-2 进程上下文切换的剖析

在执行八次上述进程上下文切换之后,hello会调用getchar。这里实际上是stdin进行系统调用read,hello会从用户模式转入内核模式,执行其他程序,实现上下文切换,同时时内核的陷阱处理程序会请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。内核从其他进程上下文切换到hello进程,程序的执行效果如图6-3。

图6-3 hello程序的执行

6.6 hello的异常与信号处理

hello程序可能会出现的异常,信号以及处理方式如下:

  • 中断:当hello程序在执行时,可能为外部I/O设备会发送信号,造成异常,这时可能会产生SIGTSTP停止信号,会调用中断处理程序,处理完成后发送SIGCONT信号,返回继续执行下一条指令,也可能发送SIGINT等终止信号,进程直接终止;
  • 陷阱:陷阱就是有意的异常,在hello调用sleep和getchar函数时都会进入陷阱,因为需要内核去执行相应操作。与中断会发送相同的停止信号,然后运行陷阱处理程序,再返回执行下一条指令;
  • 故障:执行hello进程时,可能会发生缺页故障。同样会发送停止信号,然后执行故障处理程序,如果成功修复故障,就返回重新执行当前指令,否则就会终止,这时可能会产生SIGINT, SIGKILL等信号;
  • 终止:如果遇到DRAM或者SRAM损坏这样的不可恢复的错误,就会导致异常,这时无法进行处理,只能响应SIGINT, SIGKILL等信号,该进程终止。

在运行过程中进行的各命令的结果如下:

  • 不停乱按:结果如图6-4所示,乱按只会把屏幕的输入缓存到stdin中,会与输出一起显示在屏幕上,调用getchar之后的乱按再以回车结尾都会被一起读入,随后程序执行正常终止。

图6-4 运行时不停乱按

  • Ctrl-C:结果如图6-5,在键入Ctrl-C时,内核会发送一个SIGINT信号到前台进程组的每个进程,默认情况会终止前台作业,输入fg指令会提示前台无进程,用ps查看前台进程组不会有hello进程。

图6-5 运行时键盘输入Ctrl-C的结果

  • Ctrl-Z:键入Ctrl-Z的默认结果是挂起前台的作业,这时hello进程并没有被回收,输入ps指令,仍能看到该进程,它的PID是3951(如图6-7),接着在如图6-8所示,输入jobs可以看到当前只有hello一个作业,并且显示已停止。执行pstree的结果如图6-9所示。当输入fg命令后hello回到前台继续执行(如图6-10)。当执行kill指令后,首先用ps查看进程组,发现此时hello进程还没被回收,再执行fg指令会提示当前进程已终止,再次使用ps就会发现hello的进程组已经消失,如图6-11所示,从中可以推测再次执行fg指令时,发送了SIGCHLD信号,使hello进程被回收。

图6-6 运行时键盘输入Ctrl-Z的结果

图6-7 输入Ctrl-Z 输入ps指令

图6-8 输入Ctrl-Z 输入jobs指令

图6-9 pstree指令结果

图6-10 fg指令的结果

图6-11 输入kill指令的结果

6.7本章小结

本章主要讲述了进程的概念和作用以及shell的原理。并且分析了hello进程是如何通过fork和execve函数创建、加载并运行的。接着分析了hello进程运行中如何进行上下文切换的。接着分析了hello进程运行中会出现哪些异常与信号,以及是如何处理它们的。最后尝试在运行过程中进行键盘操作并输入一系列命令进行分析hello进程对这些操作的处理方式与结果。


7hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。在实模式下逻辑地址中的段地址*16+偏移地址就会被翻译成物理地址;在保护模式下,以段描述符作为下标,到GDT/LDT表查表获得段地址,段地址+偏移地址=线性地址。

线性地址:也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址,如图5-8所示,各个指令的地址就是线性地址。

虚拟地址:也就是线性地址。

物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽象,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。

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

一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。段选择符如图7-1所示。其中 TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)。RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位于最低级的用户态,第0级高于第3级。高13位-8K个索引用来确定当前使用的段描述符在描述符表中的位置。从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加就得到了线性地址。具体流程如图7-2所示。

图7-1 段选择符

图7-2 逻辑地址到线性地址的具体流程

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

线性地址到物理地址之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。在计算机中虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。VM系统通过将虚拟内存分割为称为虚拟页的大小固定的块来处理这个问题。在任意时刻,虚拟页面的集合都被分为三个不相交的子集。未分配的:VM 系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。缓存的:当前已缓存在物理内存中的已分配页。未缓存的:未缓存在物理内存中的已分配页。

由操作系统软件、MMU(内存管理单元)中的地址翻译硬件和一个存放在物理内存中叫做页表(page table)的数据结构提供了处理虚拟页的功能。每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。页表的结构如图7-3所示。

图7-3 页表

上述是把线性地址转换为物理地址系统所提供的页表的机制,下面来具体说以下如何进行转换。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。具体流程如图7-4所示。

图7-4 使用页表的地址翻译

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

Intel Core I7在Linux系统下的地址翻译正是TLB与四级页表支持的,所以下面探讨它是如何实现的。首先,它实现支持48位的虚拟地址空间和52位的物理地址空间,TLB是虚拟地址寻址的,是四路组相联的,页的大小是4KB的,CR3控制寄存器指向第一级页表的起始位置。由一个页表大小4KB,一个PTE条目8B,共512个条目,所以使用9位二进制索引,一共4个页表共使用36位二进制索引,所以VPN 共36位,因为VA 48 位,所以VPO 12位;因为TLB共16组,所以TLBI需4位,因为VPN 36位,所以TLBT 32位。

如图7-5,CPU 产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)向TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成PA(52bit)。如果TLB中没有命中,MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO 组合成PA,并且向TLB中添加条目。如果查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

图7-5 TLB与四级页表支持的地址翻译

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

三级Cache的物理内存访问的原理都是相同的,所以下面以L1 d-cache为例进行讨论。首先它是64组的,所以组索引CI就是6位的,每个块的大小是64Bit大小的,所以偏移量CO也是6位的,整个的物理地址是52位的,那么剩下的40位就作为标记位。具体的内存访问过程如下:

  • 首先根据CPU提供的物理地址,提取出组索引,在L1 Cache中找到对应的组;
  • 然后在这组的8行当中依次匹配标记位,如果成功匹配了一行,并且该行的的有效位被设置为1,说明缓存命中,根据偏移量找到要读取的内容进行返回;
  • 如果缓存不命中,那么就会依次到L2, L3和驻村中进行寻找,进行行替换再返回到结果中。

整个的流程如图7-5的右半部分所示。

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数会在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。execve的进行内存映射的步骤如下所示:

  • 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
  • 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。图7-6概括了私有区域的不同映射。
  • 映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库 libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

图7-6 execve是如何映射用户地址空间的区域的

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

在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。图7-7展示了在缺页之前的示例页表的状态。CPU引用了VP3中的一个字,VP 3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE 3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在 PP3 中的VP4。处理过程如图7-8所示。内核会修改VP 4的页表条目,反映出VP 4不再缓存在主存中这一事实,并且会从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。

当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP3已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了。图7-9展示了在缺页之后的示例页表的状态。

图7-7 VM缺页之前

图7-8 缺页中断处理流程

图7-9 缺页处理之后

7.9动态存储分配管理

在程序运行时程序员使用动态内存分配器 (比如malloc) 获得虚拟内存。动态内存分配器维护着一个进程的虚拟内存区域,称为堆,如图7-10所示。分配器将堆视为一组不同大小的块(blocks)的集合来维护,每个块要么是已分配的,要么是空闲的。分配器有以下两种类型:

  • 显式分配器:要求应用显式地释放任何已分配的快。例如,C语言中的malloc和free;
  • 隐式分配器:应用检测到已分配块不再被程序所使用,就释放这个块,比如Java,ML和Lisp等高级语言中的垃圾收集。

图7-10 堆

显示分配器具有以下的分配条件:①处理任意的请求序列;②立即相应请求;③只使用堆;④对其块(对齐要求);⑤不修改已分配的块。

隐式空闲链表区别块的边界、已分配块和空闲块的方法如图7-11所示。这种情况下,一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。

头部后面是应用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。块的格式如图7-12所示,空闲块通过头部块的大小字段隐含地连接着,所以称这种结构为隐式空闲链表。

当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大可以放置所请求块的空闲块。分配器执行这种搜索的方式是由放置策略确定的。一些常见的策略是首次适配、下一次适配和最佳适配。

分割空闲块一旦分配器找到一个匹配的空闲块,就必须做一个另策决定,那就是分配这个块多少空间。分配器通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。如图7-13所示。如果分配器不能为请求块找到空闲块,一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。合并空闲块合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,我们只需要通过改变头部的信息就能完成合并空闲块。Knuth提出了一种采用边界标记的技术快速完成空闲块的合并如图7-14所示

图7-11 一个简单的堆块的格式

图7-12 用隐式空闲链表来组织堆

图7-13 分割一个空闲块

图7-14 使用边界标记的堆块的格式

空闲链表是将空闲块组织为某种形式的显示数据结构如图7-15所示。堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。一种方法使用后进先出的顺序维护链表,将新释放的块在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。

图7-15 使用双向空闲链表的堆块的格式

7.10本章小结

本章主要介绍hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cache的物理内存访问、进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。


8hello的IO管理

8.1 Linux的IO设备管理方法

一个Linux文件就是一个m个字节的序列:

B0, B1, B2, …, Bm-1

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

8.2 简述Unix IO接口及其函数

Unix I/O接口:

  • 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
  • Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
  • 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
  • 读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
  • 关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

Unix I/O函数:

  • int open(char* filename, int flags, mode_t mode),进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
  • int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
  • ssize_t read(int fd, void* buf, size_tn),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
  • ssize_t wirte(int fd, const void* buf, size_tn),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

8.3 printf的实现分析

首先查看printf函数的函数体:

static int printf (const char *fmt, ...)
{  
va_list args;  
int i;  
va_start(args, fmt);  
write(1,printbuf,i=vsprintf(printbuf, fmt, args));  
va_end(args);  
return i;  
}

printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。接下来是write函数:

write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL

在printf中调用系统函数write(buf, i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。

查看syscall函数体:

sys_call:  
call save


    push dword [p_proc_ready]  

    sti   
    
    push ecx   
    push ebx  
    call [sys_call_table + eax * 4]  
    add esp, 4 * 3  
    
    mov [esi + EAXREG - P_STACKBASE], eax   
    cli  
    ret

syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。

字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。

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

于是我们的打印字符串就显示在了屏幕上。

8.4 getchar的实现分析

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;  
}

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

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

8.5本章小结

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

结论

用计算机系统的语言,逐条总结hello所经历的过程。

hello.c: 最开始的源代码文件,使用C语言设计与表示。

hello.i: 对hello.c预处理之后生成的文件。

hello.s: 对hello.i进行编译处理,生成的汇编语言文件。

hello.o: 对hello.s进行汇编处理,生成的可重定位目标文件

hello: 使用链接器把hello.o和需要的共享库文件进行链接生成的可执行文件。

创建子进程:由于终端输入的不是一个内置的shell命令,因此shell调用fork()函数创建一个子进程。

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

上下文切换:hello调用sleep函数之后进程陷入内核模式,处理休眠请求,挂起当前进程,内核进行上下文切换将当前进程的控制权交给其他进程,当sleep函数调用完成时,内核执行上下文切换将控制传递给当前进程。

动态申请内存:当hello程序执行printf函数时,会调用malloc向动态内存分配器申请堆中的内存。

信号管理:程序在运行时,会接受到各种各样的信号,例如SIGINT终止信号,SIGTSTP停止信号。

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

对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

通过对以上hello生命历程的总结,我能够了解计算机设计的精巧与高效。首先计算机加大地运用的了抽象的原理从最底层的信息的表示用二进制表示抽象开始,到实现操作系统管理硬件的抽象:进程是对处理器、主存和I/O设备的抽象。虚拟内存是对主存和磁盘设备的抽象。文件是对I/O设备的抽象。这给以我启示,以后在进行设计时,使用抽象的方法能更方便抓住主要方面,方便进行设计;与此同时,设计计算机系统要考虑时间,空间还有消耗的平衡。巧妙地利用cache设计缓存系统正是完美地实现了上述平衡,不仅极大优化了性能,而且还几乎不会浪费任何的空间,同时实际实现的过程中也不会造价昂贵。


附件

文件名称

文件作用

hello.i

对hello.c预处理之后生成的文件。

hello.s

对hello.i编译生成的汇编语言文件。

hello.o

对hello.s汇编生成的可重定位目标文件

hello

对hello.o链接之后生成的可执行文件。

hello.oelf.txt

hello.o的elf格式的文本文件

hello.odump.txt

对hello.o进行反汇编生成的文本文件

helloelf.txt

hello的elf格式的文本文件

hellodump.txt

对hello进行反汇编生成的文本文件


参考文献

[1]  Randal E. Bryant, David R. O’Hallaron, 深入理解计算机系统(原书第 3 版). 北京:机械工业出版社,2016.7.

[2]  https://wwwblogs/pianist/p/3315801.html

目录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.0 伪指令

3.3.1 数据

3.3.2 赋值

3.3.3 类型转换

3.3.4 算数操作

3.3.5 关系操作

3.3.6 控制转移

3.3.7 函数操作

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.3.1 ELF Header

4.3.2 节头部表

4.3.3 .symtab

4.3.4 .rel.text

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.3.1 ELF Header

5.3.2 节头部表

5.3.3 .symtab

5.3.4 .rel.text

5.4 hello的虚拟地址空间  

5.5 链接的重定位过程分析

5.5.1 链接的过程

5.5.2 重定位的过程

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管


6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

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章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


更多推荐

ICS大作业论文

本文发布于:2023-06-14 09:35:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1461567.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:作业   论文   ICS

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!