计算机系统 大作业 题 目 程序人生-Hello’s P2P

计算机系统

大作业

题 目 程序人生-Hello’s P2P

摘 要
hello.c是所有程序员所见到的第一段代码,它看似简单易懂,实则再计算机内部不为人知之处有许多复杂的,值得我们深究的操作过程。本文将展示hello从c语言一步步走向可执行程序,再由开始执行走到进程结束被系统回收。通过见证hello.c的一生,我们将更加的深入了解计算机系统。所谓三大境界:看山是山,看水是水;看山不是山,看水不是水;看山还是山,看水还是水。希望你在见证过一步步复杂操作之后,又会从新感受到hello.c的简洁与艺术美感。

关键词:计算机系统;编译;链接;虚拟内存;进程;异常控制流

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第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简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P即from program to process。程序员输入hello.c文件,经预处理器(cpp)预处理变成hello.i,编译器(ccl)生成hello.s,汇编器(as)汇编成hello.o,链接器(ld)链接最终生成可执行目标程序hello。
之后在shell中输入./hello运行hello程序,shell 分心命令行,发现不是内置命令,就利用fork产生子进程,便完成from program to process.
020:From Zero to Zero. 调用execve 函数在新的子进程中加载并运行 hello,进入main函数执行目标代码,通过取指、译码、执行等微程序,逐步执行目标文件中的程序。当程序运行结束后, 终止的hello进程被父进程bash回收。

1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1 硬件环境
X64 CPU;1.8GHz;2G RAM;1000GHD Disk
1.2.2 软件环境
Windows10 64位;Vmware 15;Ubuntu 19.04 LTS 64位
1.2.3 开发工具
Visual Studio 2010 64位以上;GDB/OBJDUMP;DDD/EDB等

1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
名字 文件的作用
hello.c 源程序(文本文件)
hello.i 修改了的源程序(文本文件)
hello.s 汇编程序(文本文件)
hello.o 可重定位目标程序(二进制)
hello.out 可执行目标程序(二进制)

1.4 本章小结
本章是深入了解计算机系统的第一章,概述了hello.c的编译过程与其相关的工具,还有相关的在过程中生成的文件。大致了解了计算机如何从.c文件到可执行文件的过程。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
程序设计语言的预处理的概念:在编译之前进行的处理。 C语言的预处理主要有三个方面的内容: 1.宏定义; 2.文件包含; 3.条件编译。 预处理命令以符号“#”开头。
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第1行的#include < sstdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插人程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。

2.2在Ubuntu下预处理的命令
gcc hello.c -E -o hello.i
图2.1 预处理命令截图
图2.1 预处理命令截图
图2.2生成的hello.i截图
图2.2生成的hello.i截图
图2.3 生成的hello.i文件末尾源代码

2.3 Hello的预处理结果解析
由截图可看见,源代码23行由预处理变为3042行,它是将原文件中的宏进行了宏展开即将原来调用的库函数进行了展开,并且新hello.i程序中已经没有#include某个库这样的语句了。且生成的.i文件还是c语言且是文本文件。

2.4 本章小结
本章将预处理进行了细致的分析,简单的说就是预处理器在编译之前进行的处理。C语言的预处理主要有三个方面的内容:1.宏定义;2.文件包含;3.条件编译。预处理命令以符号“#”开头。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:编译器(ccl)将文本文件hello.i翻译成文本文件he11o.s,它包含一个汇编语言程序。也就是将c语言转换成汇编语言。
作用:编译程序的基本功能是把源程序(高级语言)翻译成目标程序。除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人机联系等重要功能。

3.2 在Ubuntu下编译的命令
gcc hello.i -S -o hello.s
在这里插入图片描述
图3.1 编译命令截图
在这里插入图片描述
在这里插入图片描述
图3.2 生成的hello.s文件截图
3.3 Hello的编译结果解析
3.3.1数据
(1)常量:4和8,在汇编语言中为立即数

(2)局部变量:i,argc,argv均存在栈中,位置分别为-4(%rbp),-20(%rbp), -32(%rbp)

(3)表达式:“用法: Hello 学号 姓名 秒数!\n”、"Hello %s %s\n"存在代码段

3.3.2赋值
i=0;利用movl传送语句,将立即数0传给i即-4(%rbp)

3.3.3Sizeof
i,argc均为int类型大小为4字节,argv为指针类型大小8字节
3.3.4算术操作
i++;利用addl,给i加1。

3.3.5关系操作
(1)argc!=4;利用cmpl对比立即数4与agrc进行比较

(2)i<8;利用cmpl对比立即数8与i进行比较

3.3.6数组/指针/结构操作
(1)argv[2];由于argv存在-32(%rbp)中,第一步利用movq取到argv[0],然后argv[]为指针类型大小为8位,所以%rax加16得到argv[2],再将其传给%rdx作为printf函数的第三个参数。

(2)argv[1];由于argv存在-32(%rbp)中,第一步利用movq取到argv[0],然后argv[]为指针类型大小为8位,所以%rax加8得到argv[1],再将其传给%rsi作为printf函数的第二个参数。

(3)argv[3];由于argv存在-32(%rbp)中,第一步利用movq取到argv[0],然后argv[]为指针类型大小为8位,所以%rax加24得到argv[3],再将其传给%rdi作为atoi函数的第一个参数。

3.3.7控制转移
(1)if(argc!=4){} 利用cmpl对比立即数4与agrc进行比较,若等于就跳过{},若不等就进行{}中的操作

(2)for(i=0;i<8;i++){} 
先赋初值i=0;
 
然后进行条件判断

未达到要求继续进行循环里的操作,操作完了后,改变i的值,然后又到了到条件判断

3.3.8函数操作
(1)调用exit
用%rdi作为传入的参数,然后调用exit

(2)调用printf
用%rdi,%rsi,%rdx作为传入的三个参数,然后调用printf

(3)调用atoi
用%rdi作为传入的参数,然后调用atoi,返回值存在%rax中
 
(4)调用sleep
用%rdi作为传入的参数,然后调用sleep

3.4 本章小结
本节讲述了编译器将c语言转成汇编语言,我们需要将掌握一些基本的数据结构与代码结构的转换方式,并且要能够读懂汇编语言。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序( relocatable object program)的格式,并将结果保存在目标文件hello.o中。hello.o 文件是一个二进制文件,它包含的17个字节是函数main的指令编码。如果我们在文本编辑器中打开hello.o文件,将看到一堆乱码

作用:将汇编语言翻译成机器语言方便机器执行该段代码

4.2 在Ubuntu下汇编的命令
gcc hello.s -c -o hello.o
在这里插入图片描述
图4.1汇编命令截图
4.3 可重定位目标elf格式
使用readelf -all hello.o查看所有信息
在这里插入图片描述
图4.2ELF头截图
在这里插入图片描述
图4.3节头表截图
在这里插入图片描述
图4.4符号表截图
ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、 节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
重定位节的信息首先整个重定位位于hello.o二进制文件偏移量为0x340的位置上,其每个重定位信息给予了相对首地址的偏移量,通过info提供了重定位信息。在hello中需要重定位的信息有puts,exit,printf,sleepsecs,sleep,getchar 以及rodata(存放的是跳转表)
4.4 Hello.o的结果解析
利用objdump -d -r hello.o对hello.c进行反汇编得到反汇编代码
在这里插入图片描述
这里反汇编的得到的代码与之前的得到的汇编代码有所不同,主要区别在于跳转(jmp等)与函数调用(call)后跳转位置,jmp命令给予确定的位置信息(用下条指令的地址减去需要跳转到的地址得到),而call指向rodata节中的信息而在反汇编代码中也由全零暂时代替,在重定位过后才会根据rodata信息更新该位置的值
机器语言是由命令信息寄存器操作数三种信息构成的,三者不一定都在例如call指令e8表示无符号跳转,其后4位为操作数,是地址偏移。同时操作数在机器语言中为小端存储,与汇编不一致,而分支与函数调用在机器码也需要进行计算地址偏移,但是在汇编语言中只需要标记跳转即可。

4.5 本章小结
本章讲述了.s文件转化到.o文件的操作,我们需要了解可从定位文件的格式,与各节中的存储的内容。同时我们要掌握反汇编代码与汇编代码的区别尤其是可重定位的部分,同时要对机器码有一定的了解。
(第4章1分)

第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 hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
在这里插入图片描述
图5.1链接命令截图
5.3 可执行目标文件hello的格式
在这里插入图片描述
图5.2 ELF头截图

在这里插入图片描述
在这里插入图片描述
图5.3 节头表截图
由上图,ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、 节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
节头表描述了各节的信息,其中Name对应各节的名字,Type对应记录了每节的类型 ,address对应了这一节的信息在虚拟内存中的存储位置。Offset对应了每一节相对于0x400000的偏移地址。Size记录了每一节的大小。Align记录了对齐位数,Info记录这一节的信息,后两位对应类型,前六位对应在symtab节中的ndx值。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,程序加载从0x400000开始存放由5.3中的信息可知代码段与数据段的地址为0x4010a0和0x404000
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图5.5 edb查看各段信息截图
5.5 链接的重定位过程分析
在这里插入图片描述
在这里插入图片描述
图5.6 反汇编生成汇编代码截图
由上图可见,由hello反汇编生成的代码,将程序原先调用的库函数都复制到了程序的代码中来,而重定位的部分也通过可重定位信息被真实的偏移量所填充
5.6 hello的执行流程
执行流程如下:
ld-2.27.so!_dl_start ld-2.27.so!_dl_init hello!_start
libc-2.27.so!__libc_start_main
-libc-2.27.so!__cxa_atexit
-libc-2.27.so!__libc_csu_init hello!_init libc-2.27.so!_setjmp
-libc-2.27.so!_sigsetjmp
–libc-2.27.so!__sigjmp_save hello!main hello!puts@plt hello!exit@plt
*hello!printf@plt
*hello!sleep@plt
*hello!getchar@plt ld-2.27.so!_dl_runtime_resolve_xsave
-ld-2.27.so!_dl_fixup
–ld-2.27.so!_dl_lookup_symbol_x libc-2.27.so!exit
5.7 Hello的动态链接分析
对于动态链接库中的函数,编译器不知道函数运行时的地址,就添加重定位记录,等待动态链接器处理,延迟绑定。在形成可执行文件时,还是需要用到动态链接库。在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库。在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。在dl_init调用之后,GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]指向动态链接器ld-linux.so运行时地址。

5.8 本章小结
本章讲述了hello.o链接生成hello.out的过程,我们需要了解可从定位文件hello的反汇编与可执行文件hello汇编代码的区别尤其是可重定位的部分,同时要了解main函数执行前后,一些其他函数的调用。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程就是一个执行中程序的实例
作用:进程提供给应用程序两个关键抽象(1)逻辑控制流,每个程序似乎独占地使用CPU,通过OS内核的上下文切换机制提供(2)私有地址空间,每个程序似乎独占地使用内存系统,OS内核的虚拟内存机制提供
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
其基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(3)如果不是内部命令,调用fork( )创建新进程/子进程
(4)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(5)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…)等待作业终止后返回。
(6)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
父进程通过fork创建一个子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程获得与父进程任何打开文件描述符相同的副本,这就意味着父进程调用fork时,子进程可以读写父进程任何打开的任何文件。父进程和新创建的子进程之间最大的区别在于他们拥有不同的PID。
fork被调用一次返回两次,在父进程中返回子进程的PID,在子进程中返回0。子进程与父进程并发执行并且共享文件。
hello的fork进程创建过程为:系统进程创建hello子进程然后调用waitpid()函数知道hello子进程结束。
6.4 Hello的execve过程
Execve函数在当前进程的上下文中加载并运行一个新的程序。Execve加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以与fork调用一次返回两次不一样,execve调用一次并且从不返回。
系统为hello fork子进程之后,子进程调用execve函数加载并运行可执行目标文件hello,并且带参数列表argv和环境变量列表envp,并将控制传递给main函数。以下是其详细,加载器在程序头部表的引导下将hello的片复制到代码段和数据段。接下来加载器跳转到程序的入口点——_start函数的地址,_start函数调用系统启动函数__libc_start_main(定义在libc.so中),该启动函数初始化执行环境,处理main函数返回值,在需要的时候把控制传回给内核。
6.5 Hello的进程执行
(1)进程上下文:内核为每个进程维持一个上下文。上下文就是内核重新启动一 个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
(2)进程时间片:一个进程执行它的控制的一部分的每一时间段叫做时间片
(3)进程调度的过程:保存当前进程上下文,恢复某个先前被抢占的进程被保存的上下文,将控制传递给这个新回复的进程

hello的进程执行,利用fork创建进程,然后利用execve加载并执行程序,程序初始处于用户模式,在运行到系统函数sleep时,换至内核模式,使hello进程休眠,直到时间结束,重新切换到用户态继续执行之后的指令。
6.6 hello的异常与信号处理

图6.1成功运行截图

图6.2 键盘输入ctrl z
键盘输入ctrl z发出SIGINT信号,使得hello进程挂起,利用ps可以查看到这个进程

图6.3键盘输入 ctrl c
键盘输入ctrl c 发出SIGQUIT信号,使得hello进程终止,并且被回收

图6.4键盘输入ctrl z后调用kill
键盘输入ctrl z发出SIGINT信号,使得hello进程挂起,利用ps可以查看到这个进程,然后调用kill,发送SIGKILL使得hello进程被终止,然后被回收

6.7本章小结
本章讲述了hello进程的创建,程序的加载与执行。我们需要了解进程的概念,以及其逻辑控制流和私有空间地址,同时还有清楚信号的发送与处理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
(1)逻辑地址:是指由程式产生的和段相关的偏移地址部分。例如,你在进行C语言指针编程中,能读取指针变量本身值(&操作),实际上这个值就是逻辑地址,他是相对于你当前进程数据段的地址,不和绝对物理地址相干。
(2)线性地址:是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
(3)虚拟地址:是指计算机呈现出要比实际拥有的内存大得多的内存量。因此他允许程式员编制并运行比实际系统拥有的内存大得多的程式。这使得许多大型项目也能够在具有有限内存资源的系统上实现。
(4)物理地址:是指出目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在这里插入图片描述
图7.1 段式管理
逻辑地址由段地址和偏移地址构成,由于每个段选择符大小为16位,段描述符为8字节故由逻辑地址到线性地址的变换应为 段地址0x10+偏移地址。例如0x08f10100,转换如下:0x08f10x10+0x0100=0x9010
7.3 Hello的线性地址到物理地址的变换-页式管理
在这里插入图片描述
图7.2 由虚拟地址到物理地址转换
虚拟地址由VPN和VPO两个部分组成,而物理地址由PPN和PPO两个部分组成其中PPO等于VPO,直接从虚拟地址中截取即可,而PPN由VPN查页表可得,两者合到一起得到物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
在这里插入图片描述
图7.3 多级页表地址翻译
对于四级页表来说,VPN共有36位分为4分份,每个9位,分别用于一个页表的偏移量,最终寻得PPN,而PPO等于VPO,两者合起来得到物理地址
在这里插入图片描述
图7.4 四级页表地址翻译

7.5 三级Cache支持下的物理内存访问
在这里插入图片描述
图7.5 三级Cache支持下的物理内存访问
对于每一级cache来说,将上述过程所得的物理地址分为高速缓存标记(CT)、高速缓存索引(CI)、缓存块内的字节偏移量(CO)三个部分,利用索引位找到所在组,再对比标记位,若相等且标志位为1则说明命中,则取结果给CPU,若不命中则继续去二级、三级cache中查找。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它人唯一PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct。区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚视内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
当fork函数被系统调用时,内核会为hello创建子进程,同时会创建各种数据结构并分配给hello唯一的PID。为了给hello创建虚拟内存,内核创建了当前进程的mm_struct、vm_area_struct和页表的原样副本。
7.7 hello进程execve时的内存映射
例如execve(“a.out”,NULL,NULL);
execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效地替代了当前程序。加载并运行a.out需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为a.out文件中的.text 和.data区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中栈和堆区域也是请求二进制零的,初始长度为零。
(3)映射共享区域。如果a.out程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC)。execve做的最后一件事就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
下一次调度这个进程时,它将从这个人口点开始执行。 Linux 将根据需要换人代码和数据页面。
7.8 缺页故障与缺页中断处理
在这里插入图片描述
图7.6 缺页故障处理

  1. 处理器将虚拟地址发送给 MMU
    2-3) MMU 使用内存中的页表生成PTE地址
  2. 有效位为零, 因此 MMU 触发缺页异常
  3. 缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘)
  4. 缺页处理程序调入新的页面,并更新内存中的PTE
  5. 缺页处理程序返回到原来进程,再次执行缺页的指令
    7.9动态存储分配管理
    (1)分配器的类型
    显式分配器: 要求应用显式地释放任何已分配的快
    例如,C语言中的 malloc 和 free
    隐式分配器: 应用检测到已分配块不再被程序所使用,就释放这个块
    比如Java,ML和Lisp等高级语言中的垃圾收集
    (2)记录空闲块的组织结构
    方法 1:隐式空闲链表 (Implicit list) 通过头部中的大小字段—隐含地连接所有块
    方法 2: 显式空闲链表 (Explicit list) 在空闲块中使用指针指向前后空闲块
    方法 3: 分离的空闲列表 (Segregated free list)按照大小分类,构成不同大小的空闲链表
    方法 4: 按块按大小排序—平衡树
    在每个空闲块中使用一个带指针的平衡树,并使用长度作为权值
    7.10本章小结
    本章讲述了hello的储存管理,我们需理解逻辑地址、线性地址、虚拟地址与物理地址之间的关系与转换方法。同时要理解快表与高速缓存的思想是基本一致的,最后对动态存储分配管理的各种方法要十分了解。
    (第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(1)函数原型:int open(char * filename, int flags, mode_t mode);
解析:返回值是一个文件描述符:filename顾名思义就是文件名,flage文件是打开方式,第三个形参应用于创建文件时使用。若出错都返回-1
(2)函数原型:int close(int fd);
解析:关闭一个打开的文件,若关闭一个已关闭的文件的描述符会出错。
(3)函数原型:ssize_t read(int fd ,void* buf ,size_t n);
解析:read函数从描述符为fd 的当前文件位置复制最多n个字节到内存位置buf。返回值一1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
(4)函数原型:ssize_t write(int fd, const void * buf, size_t n);
解析:write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.3 printf的实现分析
首先来看看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函数:
int vsprintf(char *buf, const char fmt, va_list args)
{
char
p;
char tmp[256];
va_list p_next_arg = args;

for (p=buf;*fmt;fmt++) { 
if (*fmt != '%') { 
*p++ = *fmt; 
continue; 
} 

fmt++; 

switch (*fmt) { 
case 'x': 
itoa(tmp, *((int*)p_next_arg)); 
strcpy(p, tmp); 
p_next_arg += 4; 
p += strlen(tmp); 
break; 
case 's': 
break; 
default: 
break; 
} 
} 

return (p - buf); 

}
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
然后是write函数:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
根据代码可知内核向寄存器传递几个参数后,中断调用syscall函数。对应ebx打印输出的buf数组中第一个元素的地址,ecx是要打印输出的个数。查看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到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),实现printf格式化输出。
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;

}
可以看到getchar函数调用了系统函数read,读入BUFSIZE字节到buf,然后返回buf的首地址,注意到只有当n = 0时才会调用read函数,如果n = 0还会返回EOF文件终止符。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回键才返回。
8.5本章小结
本章讲述了hello的IO管理,我们应了解Unix IO接口及其函数,并且理解各种基本函数的实现,例如:printf和getchar。
(第8章1分)
结论
hello的一生在计算机中的实现不过一瞬之间,但其经历的过程却远不是这一时半会儿就能说清楚的。回顾hello的一生:
1.预处理阶段,从hello.c到hello.i,预处理器将头文件直接插入到程序文本中
2.编译阶段,从hello.i到hello.s,编译器将c语言转换为汇编语言
3.汇编阶段,从hello.s到hello.o,汇编器将汇编语言转换成了机器语言
4.链接阶段,从hello.o到hello.out,链接器将可重定位文件链接成了可执行目标文件
5.可执行目标文件的执行,在shell中输入命令行,由shell调用fork创建一个新进程,再调用exevce进行加载运行
6.执行指令与访问数据均会遇到缺页中断,系统对异常进行操作处理,使其恢复运行
7.显示输出,hello调用printf函数,内核通过动态内存分配器为其分配内存
8.进程终止,shell父进程回收hello,并删除hello进程的所有痕迹。
千里之行,始于足下。hello便是千千万万程序走出的第一步。它彰显着深入浅出的美感,在未来创新中也要如hello一样,有着深奥的内在,却有着简单,平易近人的外表。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
名字 文件的作用
hello.c 源程序(文本文件)
hello.i 修改了的源程序(文本文件)
hello.s 汇编程序(文本文件)
hello.o 可重定位目标程序(二进制)
hello.out 可执行目标程序(二进制)

(附件0分,缺失 -1分)

参考文献
[1] 深入理解计算机系统 Randal E.Bryant, David R.O’Hallaron
[2] ELF文件-段和程序头 - 王文清的博客 - CSDN博客
https://blog.csdn.net/u011210147/article/details/54092405
[3] [转]printf 函数实现的深入剖析 - Pianistx - 博客园
https://www.cnblogs.com/pianist/p/3315801.html
[4] gcc常用命令选项:https://blog.csdn.net/Crazy_Tengt/article/details/71699029
[5] 关于Linux 中sleep()函数说明:
https://blog.csdn.net/fly__chen/article/details/53175301
[6] getchar()函数的返回值以及单个字符输出函数putchar:
https://blog.csdn.net/cup160828/article/details/58067647?utm_source=blogxgz9
(参考文献0分,缺失 -1分)

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值