csapp大作业hello怪折腾的一生

计算机系统

大作业

题 目 程序人生-Hello’s P2P

计算机科学与技术学院
2018年12月
摘 要
本文将从hello的诞生开始,通过hello所涉及到的过程对这些过程加以详细的介绍。
关键词:hello;csapp;生命周期;大作业;

(摘要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.c通过预处理、编译、汇编、链接过程生成了一个可执行文件hello
在shell中运行它便变成了一个进程,这便是hello从program变成process的过程,也就是P2P
shell执行hello后为其映射虚拟内存,随后载入物理内存开始执行hello的程序,将其输出显示到屏幕,随后结束进程,这便是O2O的过程
1.2 环境与工具
硬件环境:
X64CPU; 8GHz; 8GRAM; 1TB HD
软件环境:
Windows10 64位;VMware14.12; Ubuntu 16.04 LTS 64位
使用工具:
Codeblocks,objdump ,edb
1.3 中间结果
hello.i 预处理后的文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标文件
hello 链接后的可执行目标文件
hello.elf hello的elf文件
hello.txt hello.o的反汇编文件
helloasm.txt hello的反汇编文件
1.4 本章小结
本章主要介绍了这次作业的主要内容,以及开发环境等。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
主要功能:
1、 将源文件中用#include 形式声明的文件复制到新的程序中。比如 hello.c 第 6-8 行中的#include 等命令告诉预处理器读取系统头文件 stdio.h unistd.h stdlib.h 的内容,并把它直接插入到程序文本中。
2、 用实际值替换用#define 定义字符串
3、 根据#if 后面的条件决定需要编译的代码
4、特殊控制:定义了特殊的预处理指令,如#error #pragma等
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中开头的#include命令告诉预处理器读取系统头文件stdio.h中的内容,并把它直接插入程序文本中,结果就得到了另一个C程序,通常是以.i作为文件扩展名。我们打开hello.i,发现里面的确多了许多之前没有写入文本的命令,如图。

2.4 本章小结
本章主要介绍了预处理的概念及作用,对hello.c的预处理进行了解析

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s 它包含一个汇编语言程序。
作用:将高级语言编译为等价的机器语言,这里将C语言转化为intel x86汇编指令。

3.2 在Ubuntu下编译的命令
gcc -S -hello.i -o -hello.s

3.3 Hello的编译结果解析
hello.s内容如下
.file “hello.c”
.text
.globl sleepsecs
.data
.align 4
.type sleepsecs, @object
.size sleepsecs, 4
sleepsecs:
.long 2
.section .rodata
.LC0:
.string “Usage: Hello \345\255\246\345\217\267 \345\247\223\345\220\215\357\274\201”
.LC1:
.string “Hello %s %s\n”
.text
.globl main
.type main, @function
main:
.LFB5:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $3, -20(%rbp)
je .L2
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
.L2:
movl $0, -4(%rbp)
jmp .L3
.L4:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
movl sleepsecs(%rip), %eax
movl %eax, %edi
call sleep@PLT
addl $1, -4(%rbp)
.L3:
cmpl $9, -4(%rbp)
jle .L4
call getchar@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE5:
.size main, .-main
.ident “GCC: (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0”
.section .note.GNU-stack,"",@progbits

3.3.1数据
1.字符串
1)“Usage: Hello \345\255\246\345\217\267 \345\247\223\345\220\215\357\274\201”
对应原c文件中"Usage: Hello 学号 姓名!\n"
其中中文已被编码为UTF-8 格式 一个汉字占3个字节
2) “Hello %s %s\n”
对应原c文件中"Hello %s %s\n"第二个printf中的格式化参数
其中后两个字符串已在.rodata中声明
2.整数
1)int sleepsecs
.type sleepsecs, @object
.size sleepsecs, 4
sleepsecs:
.long 2
.section .rodata
这个是已经赋值的全局变量,编译器处理时在.data 节声明该变量,.data 节存放已经初始化的全局和静态 C 变量。因为是整型,所以后面给它赋的size是4个字节又设置了long类型其值为2.
2)int i
根据cmpl $9, -4(%rbp) 可以看出编译器将i存到了-4(%rbp) 中,且占4个字节
3)int argc
作为第一个参数被压栈pushq %rbp传入main函数
4)立即数
其他整型都是以立即数的形式出现
3.数组 char *argv[]
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
这是一个指针数组,从上面数组相关的指令也可以看出来,一个内容占8个字节,说明linux中一个地址的大小是8个字节
3.3.2赋值
1)int sleepsecs=2.5
全局变量直接在.data中被声明为long类型值为2(因为int类型忽略掉了小数部分)
2) i=0
movl $0, -4(%rbp)
通过这局mov指令将0赋给了i
而因为i为4字节的整数值,所以用了后缀l
3.3.3算术操作
1)i++
addl $1, -4(%rbp)
通过这个指令每次将i的值加一
3.3.4 关系判断
1)argc!=3
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $3, -20(%rbp)
je .L2
先将argc的值传入-20(%rbp) 的位置,最后与3比较。判断是否跳转L2
2)i<10
cmpl $9, -4(%rbp)
jle .L4
通过比较i与9的值,如果i小于等于9,则跳转L4继续执行循环里的内容否则退出循环
3.3.5控制转移
1)if(argc!=3)
cmpl $3, -20(%rbp)
je .L2
首先 cmpl 比较 argv 和 3,设置条件 码,使用 je 判断 ZF 标志位,如果为 0,说明 argv-3=0 argv==3,则不执行 if 中的代码直接跳转到.L2,否则 顺序执行下一条语句,即 执行 if 中的代码。
2)for(i=0;i<10;i++)
addl $1, -4(%rbp)
cmpl $9, -4(%rbp)
jle .L4
使用 cmpl 进行比较,如果 i<=9,则跳入.L4 for循环体执行,否则说明循环结束,顺序执行for之后的逻辑。
3.3.6函数
函数的调用需要有以下过程:传递控制、数据传递、分配空间
1)main函数
a.main函数被系统启动函数 __libc_start_main 调用,call指令将main函数的地址分配给%rip,随后调用main函数。
b.main函数有两个参数argc *argv[],分别使用%rdi 和%rsi 存储,函数正常出口为 return 0,将%eax 设置 0 返回。
c.使用%rbp 记录栈帧的底,函数分配栈帧空间,最后使用leave指令将栈恢复为调用之前的状态(leave相当于mov %rbp %rsp)
最后ret返回将下一条指令地址设置为%rip
2)printf函数
a.第一printf将%rdi 设置为“Usage: Hello 学号 姓名! \n”字符串的首地址。第二次 printf 设置%rdi 为“Hello %s %s\n”的首地址,设置%rsi 为 argv[1],%rdx为argv[2]。
b.第一次 printf 因为只有一个字符串参数,所以 call puts@PLT;第二次 printf 使用 call printf@PLT。
3)exit函数
a.movl $1, %edi将edi设置为1
b.call exit@PLT call调用exit
4)sleep函数
a.movl sleepsecs(%rip), %eax
movl %eax, %edi
将%edi 设置为 sleepsecs
b.call sleep@PLT
5)getchar 函数
a.call getchar@PLT

3.4 本章小结
本章主要介绍了编译器是如何处理C程序中各类数据和操作的。将hello.s中的汇编指令对应与hello.c中的C语言 可以通过hello.s中的机器指令帮助我们理解机器是如何处理C语言中的数据和操作。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将.s翻译成机器语言指令,把这些指令打包成一种叫做可充定位目标程序的格式,并将结果保存在目标文件.o中。
作用:将.s文件
4.2 在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o

4.3 可重定位目标elf格式
使用readelf -a hello.o > hello.elf 指令得到hello.o的elf格式

1) ELF header:总体描述ELF信息的段

2) Section Headers: 描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息

3) 重定位节.rela.text:一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置

4).rela.eh_frame: eh_frame 节的重定位信息。
5) .symtab符号表

4.4 Hello.o的结果解析

我们可以看到hello.o反汇编后与hello.s的区别并不大,主要区别如下:
1) 左边多了机器码,这是机器能直接识别的机器指令
2) 分支跳转语句:call后面从L1……变成了具体的地址
3) 函数调用:call后面的函数调用从.s文件中直接call函数的名字变成了call下一条语句的地址。这是因为许多函数需要从链接的库中调用,现在还不知道他们的具体地址,而此时call将他们的相对地址全都设置为0,在.rela.text中为他们设置重定位条目,等待静态链接的进一步确定。
4.5 本章小结
本章简述了汇编器的相关内容。通过readelf、objdump等方式比较了hello.s与hello.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.3 可执行目标文件hello的格式
readelf -a hello > hello.elf得到hello的elf格式文件
ELF Header:
可以看到,与上一章的elf相比,许多size都发生了变化,比如program headers、section headers等等

Section Headers:
,Section Headers 对 hello 中所有的节信息进行了声明,其 中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信 息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。

5.4 hello的虚拟地址空间
在edb中打开hello 查看data dump窗口 可以看出程序的地址是从
0x40000加载的 0x400fff结束

再看elf文件中的program headers:

每一个表项都提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐方面的信息
再看程序的八个段:
PHDR:程序头表
INTERP:程序执行前需要调用的解释器
LOAD:程序目标代码和常量信息
DYNAMIC:动态链接器所使用的信息

NOTE::辅助信息
GNU_EH_FRAME:保存异常信息
GNU_STACK:使用系统栈所需要的权限信息
GNU_RELRO:保存在重定位之后只读信息的位置
5.5 链接的重定位过程分析
对比hello和hello.o elf文件中的section headers,发现hello中多出了如下内容:
.interp:保存ld.so的路径
.note.ABI-tag Linux 下特有的 section
.note.gnu.build-i:编译信息表
.gnu.hash:gnu的扩展符号hash表
.dynsym:动态符号表
.dynstr:动态符号表中的符号名称
.gnu.version:符号版本
.gnu.version_r:符号引用版本
.rela.dyn:动态重定位
.rela.plt:.plt节的重定位条目
.init:程序初始化
.plt:动态链接表
.fini:程序终止时需要的执行的指令
.eh_frame:程序执行错误时的指令
.dynamic:存放被ld.so使用的动态链接信息
.got:存放程序中变量全局偏移
.got.plt:存放程序中函数的全局偏移量
.data:初始化过的全局变量或者声明过的函数
(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
与上一章的区别:
1) 函数个数变多:链接后多了printf、sleep等函数

2) 函数调用:call后面的指令已经变成了对应函数的正确地址
3) 函数从.text节开始变成了从init节开始
5.6 hello的执行流程
载入:
_dl_start
_dl_init
开始执行
_start
_libc_start_main
_init
执行main:
_main
_printf
_exit
_sleep
_getcha
_dl_runtime_resolve_xsave
_dl_fixup
_dl_lookup_symbol_x
退出:
exit
5.7 Hello的动态链接分析
dl_init前:

dl init后:

可以发现global表从全是0的状态被赋上了相应的偏移量的值,这说明这是初始化hello的第一步。

5.8 本章小结
本章介绍了链接的相关内容,分析了hello的elf文件,分析了hello 的虚拟地址空间、重定位过程、执行流程、动态链接过程。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
shell的作用:为用户提供一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序为其分配子进程并运行
5)shell 应该接受键盘输入信号,并对这些信号进行相应处理

6.3 Hello的fork进程创建过程
在bash中输入 ./hello 1* * 并敲击回车后,bash解析此条命令,发现./hello不是bash内置命令,于是在当前目录尝试寻找并执行hello文件。此时bash使用fork函数创建一个子进程(这个子进程得到与父进程用户级虚拟地址空间相同但是独立的一份副本),并更改这个子进程的进程组编号。并准备在这个子进程执行execve。

6.4 Hello的execve过程
在新创建的子进程中,execve函数加载并运行hello,且带参数列表argv和环境变量envp。在execve加载了hello之后,它调用_start,_start设置栈,并将控制传递给新程序的主函数。

6.5 Hello的进程执行
hello的进程执行逻辑如下图:

其中hello用户模式和内核模式相互切换的过程叫做上下文切换
最后调用getchar时先是运行在前端读取缓冲区内容,再切换到内核调用相关函数后返回hello
6.6 hello的异常与信号处理
下图为正常执行完hello的结果,执行完毕后进程被回收,再ps并看不到hello的身影

挂起进程CTRL+Z:

终止进程CTRL+C:

前台运行fg:

显示后台进程jobs:

进程树状图pstree:

kill:

kill -s 1 是发送序号为1的信号终止进程 这里是挂断

而9是杀死程序
6.7本章小结
本章介绍了进程的概念和作用,描述了shell如何在用户和系统内核之间建起一个交互的桥梁。讲述shell的基本操作以及各种内核信号和命令,还总结了shell是如何fork新建子进程、execve如何执行进程、hello进程如何在内核和前端中反复跳跃运行的。

(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序代码经过编译后出现在 汇编程序中地址。逻辑地址由选择符 (在实模式下是描述符,在保护模式下是用来选择描述符的选择符)和偏移量(偏 移部分)组成。
线性地址(linear address)或也叫虚拟地址(virtual address)
跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
物理地址(physical address)
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
——这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由两部分组成:段标识符、段内偏移量
段标识符是由一个16位长的字段组成,称为段选择符。其中前13位为索引号,后面三位包含一些硬件细节。
首先,给定一个完整的逻辑地址段选择符:段内偏移地址,
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。得到一个数组。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。
在保护模式下,分段机制就可以描述为:通过解析段寄存器中的段选择符 在段描述符表中根据 Index 选择目标描述符条目 Segment Descriptor,从目标描述 符中提取出目标段的基地址 Base address,最后加上偏移量 offset 共同构成线性地 址 Linear Address。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址被分以固定长度为单位的组,称为页(page)
例如一个32位的机器,线性地址最大可以为4G,用4KB来划分的话整个地址就被划分为2^20个页,这个数组称为页目录,目录中的每个目录项,就是对应页的地址
另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。
1、分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。万里长征就从此长始了。
2、每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中,将别个的保存下来。
3、每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)
依据以下步骤进行转换:
1、从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的地址;

7.4 TLB与四级页表支持下的VA到PA的变换
变换原理如下图
先从VA中划分出VPN和VPO ,再找到对应的TLB然后一步步递进往下寻址,越往下一层每个条目对应的区域越小,寻址越细致,在经过4层寻址之后找到相应的PPN和VPO拼接起来。

7.5 三级Cache支持下的物理内存访问
在上一步中我们得到了物理地址VA,使用CI进行组索引,每组8路,对8路的块分别匹配CT 如果匹配成功且块的有效位为1 则命中,根据CO取出数据返回
如果有效位为1但未匹配成功,则不命中,向下一级缓存中查询数据(L1>L2>L3>主存) 查到数据后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突,采用LRU策略进行替换。

(以下格式自行编排,编辑时删除)
7.6 hello进程fork时的内存映射
1.创建当前进程的内存描述符mm_struct、区域结构描述符vm_area_struct和页表的原样副本
2.两个进程的每个页面都标记为只读页面
3.两个进程的每个vm_area_struct都标记为私有

7.7 hello进程execve时的内存映射

  1. 删除已存在的用户区域
  2. 创建新的私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构
  3. 创建新的共享区域
  4. 设置PC指向代码的入口

7.8 缺页故障与缺页中断处理
当虚拟地址在MMU中查找页表时发现对应的物理内存不在主存中,需要从需要操作系统将其调入主存就称为缺页故障。
1.请求调页: 当进程调用malloc()之类的函数调用时,并未实际上分配物理内存,而是仅仅分配了一段线性地址空间,在实际访问该页框时才实际去分配物理页框,这样可以节省物理内存的开销,还有一种情况是在内存回收时,该物理页面的内容被写到了磁盘上,被系统回收了,这时候需要再分配页框,并且读取其保存的内容。
2.写时复制:当fork()一个进程时,子进程并未完整的复制父进程的地址空间,而是共享相关的资源,父进程的页表被设为只读的,当子进程进行写操作时,会触发缺页异常,从而为子进程分配页框。
3.地址范围外的错误:内核访问无效地址,用户态进程访问无效地址等。
4.内核访问非连续性地址:用于内核的高端内存映射,高端内存映射仅仅修改了主内核页表的内容,当进程访问内核态时需要将该部分的页表内容复制到自己的进程页表里面。
缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果 这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺 页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令再次发送 VA 到 MMU,这次 MMU 就能正常翻译 VA 了。

7.9动态存储分配管理
动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将内存视为一组不同大小的块的集合来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是未分配的。
分配器有两种基本风格:
1.显式分配器
要求应用显式的释放任何已分配的块。
2.隐式分配器
也叫做垃圾收集器,自动释放未使用的已分配块的过程叫做垃圾收集。

隐式空闲链表:

显式空闲链表:

空闲块合并:
因为有了 Footer,所以我们可以方便的对前面的空闲块进行合并。合并的 情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于 四种情况分别进行空闲块合并,我们只需要通过改变 Header 和 Footer 中的值 就可以完成这一操作。

分离的空闲链表:
维护多个空闲链表,其中每个链表中的块有大致相等的大小。分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小的升序排列。当分配器需要一个大小为n的块时,它就搜索相应的空闲链表。如果不能找到合适的块与其匹配,它就搜索下一个链表,以此类推。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理, 以intel Core7在指定环境下介绍了VA 到PA 的变换、物理内存访问,还介绍了hello 进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的 IO 设备都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。

8.2 简述Unix IO接口及其函数
1.open函数 打开文件:

2.close函数 关闭文件

3.read函数:read 函数从描述符为 fd 的当前文 件位置赋值最多 n 个字节到内存位置 buf

  1. 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;

}
函数的参数是一个字符指针 通过va_list_arg实现了变长的参数 因为函数接受的是指针,只要地址连续就可以读取多个参数

i = vsprintf(buf, fmt, arg);
我们查看一下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);

}

发现这个函数最后返回的是打印字符串的长度,也就是说这句话得到了字符串的长度i在下一句传给write函数
write函数如下:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL

在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALL 代表系统调用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的实现分析
函数内容:
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函数,这个read函数是将整个缓冲区都读到了buf里面,然后将返回值是缓冲区的长度。我们可以发现,如果buf长度为0,getchar才会调用read函数,否则是直接将保存的buf中的最前面的元素返回。
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数。
(第8章1分)
结论
hello同志的一生是不平凡的一生。他从诞生开始就要接受重重考验:
a. hello通过编辑器写入了hello.c正式诞生
b. 预处理器cpp将hello.c与其他库文件整合到了hello.i
c. 编译器ccl将hello.i翻译成汇编语言存入hello.s
d. 汇编器as将hello.s转化成可重定位目标文件hello.o
e. hello.o又与动态链接库链接生成可执行文件hello
f. 在shell中可通过指令 ./hello 1173710110 马健 执行hello
g. hello可以通过fork生成子进程
h. hello的内存访问有许多理论优化,如PA的cache 和VA的TLB
i. hello的io设备也有着一套复杂的实现原则
j. 在hello运行过程中,我们可以发送各种信号来控制它 如crtl+z等
k. 最后发送ctrl+c信号 hello同志辉煌的一生就此结束

(结论0分,缺少 -1分,根据内容酌情加分)

附件

hello.i      预处理后的文件
hello.s 		编译之后的汇编文件
hello.o		汇编之后的可重定位目标文件
hello 		链接后的可执行目标文件
hello.elf    hello的elf文件
hello.txt		hello.o的反汇编文件
helloasm.txt  	hello的反汇编文件

列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 兰德尔E.布莱恩特等. 深入理解计算机系统. 北京:机械工业出版社,2016:1-640.
[2] printf函数实现的深入剖析:
https://blog.csdn.net/zhengqijun_/article/details/72454714
[3] getchar百度百科:
https://baike.baidu.com/item/getchar/919709?fr=aladdin
[4] LINUX 逻辑地址、线性地址、物理地址和虚拟地址
https://www.cnblogs.com/zengkefu/p/5452792.html

(参考文献0分,缺少 -1分)

你可能感兴趣的:(csapp大作业hello怪折腾的一生)