程序人生-Hello’s P2P

程序人生-Hello’s P2P

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 软件工程

计算机科学与技术学院
2019年12月
摘 要
本文遍历了hello.c在Linux下生命周期,借助Linux下系列开发工具,通过对其预处理、编译、汇编等过程的分步解读及对比来学习各个过程在Linux下实现机制及原因。同时通过对hello在Shell中的动态链接、进程运行、内存管理、I/O管理等过程的探索来更深层次的理解Linux系统下的动态链接机制、存储层次结构、异常控制流、虚拟内存及UnixI/O等相关内容。旨在将课本知识与实例结合学习,更加深入地理解计算机系统的课程内容。
关键词:编译系统;进程;信号与异常;内存管理;Linux I/O管理;

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

目 录

第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 5 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在Ubuntu下预处理的命令 - 6 -
2.3 Hello的预处理结果解析 - 6 -
2.4 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在Ubuntu下编译的命令 - 8 -
3.3 Hello的编译结果解析 - 9 -
3.4 本章小结 - 13 -
第4章 汇编 - 14 -
4.1 汇编的概念与作用 - 14 -
4.2 在Ubuntu下汇编的命令 - 14 -
4.3 可重定位目标elf格式 - 14 -
4.4 Hello.o的结果解析 - 16 -
4.5 本章小结 - 17 -
第5章 链接 - 19 -
5.1 链接的概念与作用 - 19 -
5.2 在Ubuntu下链接的命令 - 19 -
5.3 可执行目标文件hello的格式 - 19 -
5.4 hello的虚拟地址空间 - 20 -
5.5 链接的重定位过程分析 - 21 -
5.6 hello的执行流程 - 21 -
5.7 Hello的动态链接分析 - 23 -
5.8 本章小结 - 23 -
第6章 hello进程管理 - 26 -
6.1 进程的概念与作用 - 26 -
6.2 简述壳Shell-bash的作用与处理流程 - 26 -
6.3 Hello的fork进程创建过程 - 26 -
6.4 Hello的execve过程 - 27 -
6.5 Hello的进程执行 - 27 -
6.6 hello的异常与信号处理 - 27 -
6.7本章小结 - 31 -
第7章 hello的存储管理 - 32 -
7.1 hello的存储器地址空间 - 32 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 32 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 32 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 36 -
7.5 三级Cache支持下的物理内存访问 - 38 -
7.6 hello进程fork时的内存映射 - 39 -
7.7 hello进程execve时的内存映射 - 39 -
7.8 缺页故障与缺页中断处理 - 40 -
7.9动态存储分配管理 - 41 -
7.10本章小结 - 42 -
第8章 hello的IO管理 - 43 -
8.1 Linux的IO设备管理方法 - 43 -
8.2 简述Unix IO接口及其函数 - 43 -
8.3 printf的实现分析 - 43 -
8.4 getchar的实现分析 - 46 -
8.5本章小结 - 46 -
结论 - 46 -
附件 - 48 -
参考文献 - 49 -

第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:From Program to Process

gcc编译器驱动程序读取程序文件hello.c,然后由预处理器(cpp)根据以#字符开头的命令,修改该程序获得hello.i文件,通过编译器(cll)将文本文件hello.i翻译成文本文件形式的汇编程序hello.s,再通过汇编器(cs)将hello.s翻译成机器语言指令,将指令打包成可重定位的目标文件hello.o,然后通过链接器(ld)合并获得可执行目标文件hello。Linux系统中通过内置命令行解释器shell加载运行hello程序,为hello程序fork进程,至此,hello.c完成了P2P的过程。

O2O:From Zero-0 to Zero -0

Shell通过execve在fork产生的子进程中加载hello,先删除当前虚拟地址的用户部分已存在的数据结构,为hello的代码、数据、bss和栈区域创建新的区域结构,然后映射共享区域,设置程序计数器,使之指向代码区域的入口点,进入main函数,CPU为hello分配时间片执行逻辑控制流。hello通过Unix I/O管理来控制输出。hello执行完成后shell会回收hello进程,并且内核会从系统中删除hello所有痕迹,至此,hello完成O2O的过程。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:Intel(R)Core™i5-8300U CPU 2.30GHx 2.30GHz 8G RAM
软件环境:Windows 10 家庭中文版,Ubuntu 18.04.1 LTS(VMware)
工具:codeblocks,gedit,gcc,objdump,readelf 等
1.3 中间结果
文件名称 文件作用
hello.i 预处理之后文本文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标执行
hello 链接之后的可执行目标文件
helloo.objdmp Hello.o 的反汇编代码
helloo.elf Hello.o 的 ELF 格式
hello.objdmp Hello 的反汇编代码
hello.elf Hellode ELF 格式
1.4 本章小结
hello.c程序从编写、预处理、编译、汇编、链接再到执行,体现了计算机系统系统各部分的具体功能,以及它们之间的的协同合作。

(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:预处理器 cpp 根据以字符#开头的命令(宏定义、条件编译),修改原 始的 C 程序,将引用的所有库展开合并成为一个完整的文本文件。
主要功能如下:
1、 将源文件中用#include 形式声明的文件复制到新的程序中。比如 hello.c 第 6-8 行中的#include<stdio.h> 等命令告诉预处理器读取系统头文件 stdio.h unistd.h stdlib.h 的内容,并把它直接插入到程序文本中。
2、用实际值替换用#define定义的字符串
3、 根据#if 后面的条件决定需要编译的代码
2.2在Ubuntu下预处理的命令
在这里插入图片描述

图 2.1 使用 cpp 命令生成 hello.i 文件
2.3 Hello的预处理结果解析
我们在2.1节中说到过,预处理只对开头是#的命令进行操作。也就是说,对于我们程序中定义的变量、写的函数等这些操作,预处理阶段是不会管的,我们首先就来对比一下这一部分。如图2.2所示,在预处理之前,程序中包含开始的注释内容、头文件、全局变量和主函数。而右侧是预处理过后的文件,这里展示了文件的最后几行,可以看到,从全局变量的定义开始,与预处理之前的文件完全相同,这与2.1中的概念相符。
在这里插入图片描述
在这里插入图片描述

图 2.2 预处理前后的程序文本
接下来我们回到hello.i文件的开头,从图2.3中可以看出,hello.i程序中并没有了注释部分。最后我们再来看hello.i文本的中间部分,首先我们看到左侧的图中从第13行开始有很多的地址,还有如右侧图中的一些代码部分。这说明,预处理阶段,预处理器将需要用到的库的地址和库中的函数加入到了文本中,与我们原来不需要预处理的代码一同构成了hello.i文件,用来被编译器继续进行编译。
在这里插入图片描述
在这里插入图片描述

图2.3 hello.i的文件内容
2.4 本章小结
本阶段完成了对hello.c的预处理工作。使用Ubuntu下的预处理指令可以将其转换为.i文件。完成该阶段转换后,可以进行下一阶段的汇编处理。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用

概念: 编译是利用编译程序从预处理文本文件产生汇编程序(文本)的过程。主要包含五个阶段:词法分析;语法分析;语义检查、中间代码生成、目标代码生成。

作用:编译作用主要是将文本文件hello.i翻译成文本文件hello.s,并在出现语法错误时给出提示信息,执行过程主要从其中三个阶段进行分析:

  1. 词法分析。词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序;
  2. 语法分析。语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位;
  3. 目标代码生成。目标代码生成器把语法分析后或优化后的中间代码经汇编程序汇编生成汇编语言代码,成为可执行的机器语言代码。

3.2 在Ubuntu下编译的命令
命令:gcc -S hello.c -o hello.s
在这里插入图片描述
图 3.1 使用 gcc 命令生成 64 位的 hello.s 文件
3.3 Hello的编译结果解析
指令 含义
.file 声明源文件
.text 以下是代码段
.section .rodata 以下是 rodata 节
.globl 声明一个全局变量
.type 用来指定是函数类型或是对象类型
.size 声明大小
.long、.string 声明一个 long、string 类型
.align 声明对指令或者数据的存放地址进行对齐的方式
在这里插入图片描述
在这里插入图片描述

图 3.2 hello.s文件
3.3.1数据
一.整数.
1)局部变量:int i; 编译器将局部变量存储在寄存器或者栈空间中。
从3.2图可以看出编译器将i存储在栈上空间-4(%rbp)中,默认被初始化为0,并且未被分配空间,放在.rodata节中,只有在需要使用的时候才为其分配空间。
2)int argc:作为第一个参数传入。
3) 立即数:其他整形数据的出现都是以立即数的形式出现的,直接硬编码在汇编代码中。
二.字符串
.LC0:
.string “\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201”
.LC1:
.string “Hello %s %s\n”
第一个字符串被编码成UTF-8格式,一个汉字在utf-8编码中占三个字节,一个\代表一个字节。
恰好对应
第二个字符串"Hello %s %s\n"
编译时这两个字符串保存在.rodata中
三.数组
Hello程序中访问了argv数组中的三个元素。在汇编文件中是通过改变基址偏移量来访问相应元素的。
在这里插入图片描述
图3.3
如图3.3所示,图中的便是argv数组的起始地址-32(%rbp),通过对临时寄存器%rax加8字节来访问argv[1],加16字节访问argv[2],加24字节访问argv[3]。
3.3.2赋值
i=0:整型数据的赋值使用 mov 指令完成,根据数据的大小不同使用不同后缀,分别为:
指令b w l q 大小
8b (1B) 16b (2B) 32b (4B) 64b (8B)
因为 i 是 4B 的 int 类型,所以使用 movl 进行赋值,汇编代码:
.L2:
movl $0, -4(%rbp)
3.3.3算术操作
Hello程序中涉及到的算术操作是for循环控制变量i的自增操作,操作符为++。这里对它的处理也十分简单,使用了addq指令对变量i (-4(%rbp))直接加一的方法。
3.3.4关系操作
1.
.L3:
cmpl $7, -4(%rbp)
jle .L4
C源代码中的语句是i<8 而这里的语句的等效C语句确是i<=7,由于编译器会对C代码进行优化
2.
cmpl $4, -20(%rbp)
je .L2
将argc与4作比较,相等则跳.L2,等价于C中的:
if(argc!=4){
printf(“用法: Hello 学号 姓名 秒数!\n”);
exit(1);
}
3.3.5 数组操作
通过对传入的字符串数组argv进行寻址来读取参数。argv是从命令行键入的字符串的地址数组,里面按顺序存放着命令行输入的字符串在内存中的存放地址。
argv[2]地址
argv[1]地址
3.3.6 控制转移
1.if
设置条件码,判断ZF零标志,如果最近的操作得出的结果为0,则跳到.L2中,否则顺序执行下一条语句。代码:
cmpl $4, -20(%rbp)
je .L2
2.for循环
for循环的控制时比较7, -4(%rbp) ,当i大于7时跳出循环,否则进入.L4执行,代码:
.L3:
cmpl $7, -4(%rbp)
jle .L4

3.3.7 函数操作
函数是一种过程,过程提供了一种封装代码的方式,用一组指定的参数和可选 的返回值实现某种功能。P 中调用函数 Q 包含以下动作:
1)传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始 地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的 地址。
2) 传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回 一个值。
3) 分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。

64 位程序参数存储顺序(浮点数使用 xmm,不包含):
1 2 3 4 5 6 7
%rdi %rsi %rdx %rcx %r8 %r9 栈空间
程序中涉及函数操作的有:
1) main 函数:
a)传递控制,main 函数因为被调用 call 才能执行(被系统启动函数 __libc_start_main 调用),call 指令将下一条指令的地址 dest 压栈, 然后跳转到 main 函数。
b) 传递数据,外部调用过程向 main 函数传递参数 argc 和 argv,分别使用%rdi 和%rsi存储,函数正常出口为 return 0,将%eax 设置 0 返回。
c) 分配和释放内存,使用%rbp记录栈帧的底,函数分配栈帧空间 在%rbp 之上,程序结束时,调用 leave 指令,leave 相当于 mov %rbp,%rsp,pop %rbp,恢复栈空间为调用之前的状态,然后 ret 返回,ret 相当 pop IP,将下一条要执行指令的地址设置为 dest。
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) 传递数据:将%edi 设置为 1。
b) 控制传递:call exit@PLT。
4) sleep 函数:
a) 传递数据:传入参数argv[3]
b) 控制传递:call sleep@PLT。
5) getchar 函数:
a) 控制传递:call gethcar@PLT

3.4 本章小结
编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
通过对编译的结果进行解析,更深刻地理解了C语言的数据与操作,并且对C语言翻译成汇编语言有了更好的掌握。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用

汇编器(as)将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位 目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它 包含程序的指令编码。这个过程称为汇编,亦即汇编的作用
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
在这里插入图片描述
图4.1 生成.o
在这里插入图片描述
图4.2 生成的.o
4.3 可重定位目标elf格式
使用 readelf -a hello.o > helloo.elf 指令获得 hello.o 文件的 ELF 格式。
其组成如下:
1) ELF Header:以 16B 的序列 Magic 开始,Magic描述了生成该文件的系统 的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解 释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,以及节头部表中条目的大 小和数量等信息。
在这里插入图片描述
图4.3 ELF Header
2)Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。
在这里插入图片描述
图4.4 节头部表
3)重定位节.rela.text ,一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
在这里插入图片描述
图4.5 重定位节
偏移量(offset):需要被修改的引用的节的偏移。
信息(info):包括 symbol 和 type 两部分, 其中 symbol 占前 4 个字节, type占后 4 个字节,symbol 代表重定位到的目标在.symtab中的偏移量,type 代表重定位的类型。
类型(type):告知链接器如何修改新的引用。
符号名称(name):重定位目标的名称。
加数(addend):一个有符号常熟,一些类型的重定位要使用它对被修改引用的值做便宜调整。
4).rela.eh_frame : eh_frame 节的重定位信息
5).symtab:符号表,用来存放程序中定义和引用的函数和全局变量的信息。重定位需要引用的符号都在其中声明
在这里插入图片描述
图4.6 符号表

4.4 Hello.o的结果解析
总体观察图 4.7 后发现,除去显示格式之外两者差别不大,主要差别如下:
1)分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
2) 函数调用:在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call 的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其 call 指令后的相对地址设置为全 0(目标地址正是下一条指令),然 后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。
3) 全局变量访问:在.s 文件中,访问 rodata(printf 中的字符串),使用段名称+%rip,在反汇编代码中 0+%rip,因为 rodata 中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全 0 并添加重定位条目
在这里插入图片描述
在这里插入图片描述

图4.7 objdump后的文件
4.5 本章小结
本章分析了hello.o的elf格式,简介了汇编器对hello.s的处理结果,通过对比hello.o反汇编后的文本和hello.s的异同,分析了机器语言和汇编语言的映射关系。
(第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的格式
使用 readelf -a hello > hello.elf 命令生成 hello 程序的 ELF 格式文件
在 ELF 格式文件中,Section Headers 对 hello 中所有的节信息进行了声明,其中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。
在这里插入图片描述在这里插入图片描述

图5.2 hello ELF 格式中的 Section Headers
5.4 hello的虚拟地址空间
使用 edb 打开 hello 程序,通过 edb 的 Data Dump 窗口查看加载到虚拟地址中的 hello 程序。 在 0x400000~0x401000 段中,程序被载入,自虚拟地址 0x400000 开始,自 0x400fff 结束,这之间每个节(开始 ~ .eh_frame 节)的排列即开始结束同图 5.2 中 Address 中声明。 如图 5.3,查看 ELF 格式文件中的 Program Headers,程序头表在执行的时候被使用,它告诉链接器运行时加载的内容并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐方面的信息。在下面可以看出,程序包含 8 个段:
1)PHDR 保存程序头表。
2)INTERP 指定在程序已经从可执行文件映射到内存之后,必须调用的解释 器(如动态链接器)。
3)LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了 常量数据(如字符串)、程序的目标代码等。
4)DYNAMIC 保存了由动态链接器使用的信息。
5) NOTE 保存辅助信息。
6) GNU_STACK:权限标志,标志栈是否是可执行的。
7) GNU_RELRO:指定在重定位结束之后那些内存区域是需要设置只读。
在这里插入图片描述
图5.3 ELF 格式文件中的 Program Headers
通过 Data Dump 查看虚拟地址段 0x600000~0x602000,在 0~fff 空间中,与 0x400000~0x401000 段的存放的程序相同,在 fff 之后存放的是.dynamic~.shstrtab 节。
5.5 链接的重定位过程分析
与 hello.o 反汇编文本 helloo.objdump 相比,在 hello.objdump 中多了许多节.
在这里插入图片描述
图5.4 部分hello.objdump
区别:

hello可执行目标文件中多出了.init段和.plt段。.init段用于初始化程序执行环境,.plt段是程序执行时的动态链接。所有的重定位条目都被修改为了确定的运行时内存地址。
程序添加了许多动态链接库中的函数。使用ld链接,定义了函数入口,初始化函数,动态链接器与动态链接共享库定义hello.o中的各种函数,将上述共享函数加入。
hello.o中的相对偏移地址到了hello中变成了虚拟内存地址,hello中使用的跳转地址和函数调用地址均为虚拟内存地址。

链接过程:在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main。链接器将上述函数加入。

重定位:合并输入模块并为每个符号分配运行时地址。包括重定位节和符号定义和重定位节中的符号引用。其中在重定位节和符号中链接器将所有相同类型的节合并为同一类型的新的聚合节。连接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号因此全局变量都有唯一的运行时内存地址。而在重定位节中链接器依赖于hello.o中的重定位条目,修改代码节和数据节中对每个符号的引用,使得它们指向正确运行时的地址。

5.6 hello的执行流程
在这里插入图片描述

图5.5 使用edb查看的部分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!atoi@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的动态链接分析
对于动态共享链接库中 PIC 函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,GNU 编译系统使用延迟绑定技 术,将过程地址的绑定推迟到第一次调用该过程时。 使用延迟绑定的动机是对于一个像 libc.so 这样的共享库输出的成百上千个函数中,一个典型的应用程序只会使用其中很少的一部分。把函数地址的解析推迟到 它实际被调用的地方,能避免动态链接器在加载时进行成百上千个其实并不需要 的重定位。第一次调用过程的运行时开销很大,但是其后的每次调用都只会花费一条指令和一个间接的内存引用。 延迟绑定是通过两个数据结构的交互来实现的,这两个数据结构是 GOT(全局偏移量表)和 PLT(过程链接表)。如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的 GOT 和 PLT。GOT 是数据段的一部分,PLT 是代码段的一部分。图 5.6 介绍了 GOT 和 PLT 交互的一个例子。只需注意:GOT 和 PLT 联合使用时,GOT[0]和GOT[1]包含动态连接器在解析函数地址时会使用的信息。 GOT[2]是动态连接器在 ld-linux.so 模块中的入口点
在这里插入图片描述
图5.6 用 PLT 和 GOT 调用外部函数
接下来观察 dl_init 前后动态链接项目的变化。由图 5.2 可以得知.got.plt 节的起 始地址是 0x404000,在 DataDump 中找到该位置
在这里插入图片描述

图5.7 dl_init 前的.got.plt 节
在这里插入图片描述
图5.8dl_init 后的.got.plt 节
可以看到 dl_init 后出现了两个地址,0x7f948f9c9190 和 0x7f948f9b4200。这就 是 GOT[1]和 GOT[2]。进入 GOT[2],应该就是上文提到的动态链接函数的地址, 用 Follow address in CPU 查看该地址的内容,发现是动态链接函数:
在这里插入图片描述
图5.9 动态链接函数
在之后的函数调用时,首先跳转到 PLT 执行.plt 中逻辑,第一次访问跳转时 GOT 地址为下一条指令,将函数序号压栈,然后跳转到 PLT[0],在 PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写 GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。
因为在 PLT 中使用的 jmp,所以执行完目标函数之后的返回地址为最近 call 指 令下一条指令地址,即在 main 中的调用完成地址。

5.8 本章小结
本章中介绍了链接的概念和作用,通过分析hello程序的虚拟空间,链接过程(重定位),执行流程,动态链接,介绍了动态链接的过程。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:一个执行中的程序的实例,同时也是系统进行资源分配和调度的基本单位。一般情况下,包括文本区域、数据区域和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
进程的作用:给予应用程序关键抽象:一个独立的逻辑流,它提供一个假象,好像我们的程序独占地使用处理器;一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
shell-bash 的作用:shell-bash 是一个 C 语言程序,是用户使用 Unix/Linux 的 桥梁,它交互性地解释和执行用户输入的命令,能够通过调用系统级的函数或功能 来执行程序、建立文件、进行并行操作等等。同时它也能够协调程序间的运行冲突,保证程序能够以并行形式高效执行。bash 还提供了一个图形化界面,提升交互的速度。
shell-bash 的处理流程:
1.从终端或控制台获取用户输入的命令
2.对读入的命令进行分割并重构命令参数
3.如果是内部命令则调用内部函数来执行
4.否则执行外部程序
5. 判断程序的执行状态是前台还是后台,若为前台进程则等待进程结束;否则直接将进程放入后台执行,继续等待用户的下一次输入。
6.3 Hello的fork进程创建过程
在终端 Gnome-Terminal 中键入 ./hello 1181910425 于春雨 2,运行的终端程序会 对输入的命令行进行解析,因为 hello 不是一个内置的 shell 命令所以解析之后终端程序判断./hello 的语义为执行当前目录下的可执行目标文件 hello,之后终端程序首先会调用 fork 函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们拥有不同的 PID。 父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
接下来 hello 将在 fork 创建的子进程中执行。 流程图如下:
在这里插入图片描述

6.4 Hello的execve过程
fork 之后,shell 在子进程中调用 execve 函数,在当前进程的上下文中加载并运行 hello 程序,execve 调用驻留在内存中的被称为启动加载器的操作系统代码来 执行 hello 程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数 据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容,然后跳转到_start,_start 函数调用系统启动函数__libc_start_main 来初始化环境,调用 用户层中 hello 的 main 函数,并在需要的时候将控制返回给内核。
6.5 Hello的进程执行
操作系统内核使用一中称为上下文切换的较高层形式的异常控制流来实现多任务:内核为每个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进 程所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程一打开文件的信息的文件表。上下文切换的流程是:1.保存当前进程的上下文。2.恢复某个先前被抢占的进程被保存的上下文。3.将控制传递给这个新恢复的进程。
在这里插入图片描述

图 6.1 进程的上下文切换
为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制, 限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理器通常使用 某个控制寄存器的一个模式位提供两种模式的区分,该寄存器描述了进程当前享 有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统 中的任何内存位置。 接下来分析 hello 的进程调度,hello 在刚开始运行时内核为其保存一个上下文, 进程在用户状态下运行。如果没有异常或中断信号的产生,hello将继续正常地执行。如果有异常或系统中断,那么内核将启用调度器休眠当前进程,并在内核模式中完成上下文切换,将控制传递给其他进程。当 hello 运行到 sleep(atoi(argv[3]))时, hello 显式地请求休眠,并发生上下文切换,控制转移给另一个进程,此时计时器开始计时,当计时器到达2s 时,它会产生一个中断信号,中断当前正在进行的进程,进行上下文切换,恢复 hello 在休眠前的上下文信息,控制权回到 hello 继续执行。当循环结束后,hello 调用 getchar 函数,之前 hello 运行在用户模式下,在调 用 getchar 时进入内核模式,内核中的陷阱处理程序请求来自键盘缓冲区的 DMA 传输,并执行上下文切换,并把控制转移给其他进程。当完成键盘缓冲区到内存的 数据传输后,引发一个中断信号,此时内核从其他进程切换回 hello 进程,然后 hello 执行 return,进程终止。
6.6 hello的异常与信号处理
如图 6.2(a),是正常执行 hello 程序的结果,当程序执行完成之后,进程被 回收。
如图 6.2(b),是在程序输出 2 条 info 之后按下 ctrl-z 的结果,当按下 ctrl-z 之后,shell 父进程收到 SIGSTP 信号,信号处理函数的逻辑是打印屏幕回显、将 hello 进程挂起,通过 ps 命令我们可以看出 hello 进程没有被回收,此时他的后台 job 号是 1,调用 fg 1 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 8 条 info,之后输入字串,程序结束,同时进程被回收。
如图 6.2(c)是在程序输出 2条 info 之后按下 ctrl-c 的结果,当按下 ctrl-c 之 后,shell 父进程收到 SIGINT 信号,信号处理函数的逻辑是结束 hello,并回收 hello 进程。
如图 6.2(d)是在程序运行中途乱按的结果,可以发现,乱按只是将屏幕的输入缓存到 stdin,当 getchar 的时候读出一个’\n’结尾的字串(作为一次输入),其 他字串会当做 shell 命令行输入。
在这里插入图片描述
图 6.2 (a) 正常运行 hello 程序
在这里插入图片描述

图 6.2(b)运行中途按下 ctrl-z
在这里插入图片描述
图 6.2(c)运行中途按下 ctrl-c
在这里插入图片描述
图 6.2(d)运行中途乱按
在这里插入图片描述
图 6.2(e)用 kill 命令发送 SIGKILL 杀死进程

6.7本章小结
本章介绍了Hello程序是如何在shell中运行的:以进程形式。介绍了shell-bash的作用和一般处理流程。介绍了进程中两个关键的抽象:逻辑控制流和私有空间。并且通过分析hello进程的创建,加载和运行进一步介绍了上下文切换的机制,通过运行hello程序,介绍了异常控制流的通知机制信号。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量, 表示为 [段标识符:段内偏移量]
对于hello程序来说即是hello.o里面的相对偏移地址
线性地址:逻辑地址经过段机制后转化为线性地址,为[描述符:偏移量]的组合形式。分页机制中线性地址作为输入。
虚拟地址:CPU在寻址的时候,是按照虚拟地址来寻址,然后通过MMU(内存管理单元)将虚拟地址转换为物理地址。即在前文中看到的hello的虚拟内存地址。
物理地址:计算机主存被组织成由M个连续字节大小的内存组成的数组,每个字节都有一个唯一的地址,该地址被称为物理地址。对于hello来说是在其运行时由MMU根据虚拟内存映射到的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址空间表示:段地址:偏移地址。 在实模式下:逻辑地址 CS:EA=CS*16+EA 物理地址 在保护模式下:以段描述符作为下标,到 GDT/LDT 表查表获得段地址, 段地址+偏移地址=线性地址。段内偏移量是在链接后就已经得到的 32 位地址,因此要想由逻辑地址得到线 性地址,需要根据逻辑地址的前 16 位获得段地址,这 16 位存放在段寄存器中。 段寄存器(16 位):用于存放段选择符 CS(代码段):程序代码所在段 SS(栈段):栈区所在段 DS(数据段):全局静态数据区所在段其他三个段寄存器 ES、GS 和 FS 可指向任意数据段。 段选择符中字段的含义:
在这里插入图片描述
其中 CS 寄存器中的 RPL 字段表示 CPU 的当前特权级 TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)
RPL=00 为第 0 级,位于最高级的内核态;RPL=11 为第 3 级,位于最低级的用户态高13 位-8K个索引用来确定当前使用的段描述符在描述符表中的位置。
在这里插入图片描述

图 7.1 段寄存器的含义
段描述符是一种数据结构,等价于段表项,分为两类。一类是用户的代码段和数据段描述符,一类是系统控制段描述符。
描述符表:实际上为段表,由段描述符(段表项构成)分为三种类型:
全局描述符表 GDT:只有一个,用来存放系统内每个任务都可能访问的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及 TSS(任务状 态段)等都属于 GDT 中描述的段
局部描述符表 LDT:存放某任务(即用户进程)专用的描述符中断描述符表
IDT:包含 256 个中断门、陷阱门和任务门描述符
下图展示了逻辑地址到线性地址的转化过程
在这里插入图片描述
图 7.2 逻辑地址到线性地址的转化过程
首先根据段选择符的 TI 部分判断需要用到的段选择符表是全局描述符表还是 局部描述符表,随后根据段选择符的高 13 位的索引(描述符表偏移)到对应的描述符表中找到对应的偏移量的段描述符,从中取出 32 位的段基址地址,将 32 位的段基址地址与 32 位的段内偏移量相加得到 32 位的线性地址。

7.3 Hello的线性地址到物理地址的变换-页式管理
概念上而言,虚拟内存被组织为一个由存放在磁盘上的 N 个连续的字节大小 的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上 数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘(较低层) 上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。VM 系 统通过将虚拟内存分割位称为虚拟页的大小固定的块来处理这个问题。每个虚拟 页的大小位 P = 2p 字节。类似地,物理内存被分割为物理页,大小也为 P 字节。 虚拟页面地集合被分为三个不相交的子集:已缓存、未缓存和未分配。
在这里插入图片描述
图 7.3 虚拟页与物理页
下图展示了页式管理中虚拟地址到物理地址的转换:
在这里插入图片描述
图 7.4 虚拟地址到物理地址
虚拟地址分为两部分:前一部分为虚拟页号,可以索引到当前进程的的物 理页表地址,后一部分为虚拟页偏移量,将来可以直接作为物理页偏移量,页表是一个存放在物理内存中的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。
在这里插入图片描述
图 7.5 页表
图 7.5 展示了一个页表的基本组织结构。虚拟地址空间中的每个页在页表中一个固定偏移量的位置都有一个 PTE(页表条目),而每个 PTE 是由一个有效位和一个 n 位的地址字段组成的。页表 PTE 分为三种情况:
1.已分配:PTE 有效位为 1 且地址部分不为 null,即页面已被分配,将一个 虚拟地址映射到了一个对应的物理地址
2.未缓冲:PTE 有效位为 0 且地址部分不为 null,即页面已经对应了一个虚 拟地址,但虚拟内存内容还未缓存到物理内存中
3.未分配:PTE 有效位为 0 且地址部分为 null,即页面还未分配,没有建立映射关系
现在根据图 7.4 介绍虚拟地址转换为物理地址的过程:首先根据虚拟页号在当前进程的物理页表中找到对应的页面,若符号位设置为 1,则表示命中,从页面中取出物理页号+虚拟页偏移量即组成了一个物理地址;否则表示不命中,产生一个缺页异常,需要从磁盘中读取相应的物理页到内存。
7.4 TLB与四级页表支持下的VA到PA的变换
为了消除每次 CPU 产生一个虚拟地址,MMU 就查阅一个 PTE 带来的时间开 销,许多系统都在 MMU 中包括了一个关于 PTE 的小的缓存,称为翻译后被缓冲 器(TLB),TLB 的速度快于 L1 cache。
在这里插入图片描述
图 7.6 虚拟地址中用以访问 TLB 的组成部分
TLB 通过虚拟地址 VPN 部分进行索引,分为索引(TLBI)与标记(TLBT)两个部分。这样,MMU 在读取 PTE 时会直接通过 TLB,如果不命中再从内存中将 PTE 复制到 TLB。 同时,为了减少页表太大而造成的空间损失,可以使用层次结构的页表页压缩 页表大小。core i7 使用的是四级页表。
在这里插入图片描述
图 7.7 一个二级页表层次结构
在四级页表层次结构的地址翻译中,虚拟地址被划分为 4 个 VPN 和 1 个 VPO。每个 VPNi 都是一个到第 i 级页表的索引,第 j 级页表中的每个 PTE 都指向第 j+1 级某个页表的基址,第四级页表中的每个 PTE 包含某个物理页面的 PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定 PPN 之前,MMU 必须访问四个PTE。
在这里插入图片描述
图 7.8 使用四级页表的地址翻译
综上,在四级页表下,MMU 根据虚拟地址不同段的数字通过 TLB 快速访问得到下 一级页表的索引或者得到第四级页表中的物理页表然后与 VPO 组合,得到物理地 址(PA)
7.5 三级Cache支持下的物理内存访问
前提:只讨论 L1 Cache 的寻址细节,L2 与 L3Cache 原理相同。L1 Cache 是 8 路 64 组相联。块大小为 64B。 解析前提条件:因为共 64 组,所以需要 6bit CI进行组寻址,因为共有 8 路, 因为块大小为 64B 所以需要 6bit CO 表示数据偏移位置,因为 VA 共 52bit,所以 CT 共 40bit。

在上一步中我们已经获得了物理地址 VA,如图 7.9,使用 CI(后六位再后六 位)进行组索引,每组 8 路,对 8 路的块分别匹配 CT(前 40 位)如果匹配成功且块的 valid 标志位为 1,则命中(hit),根据数据偏移量 CO(后六位)取出数 据返回。 如果没有匹配成功或者匹配成功但是标志位是 1,则不命中(miss),向下一 级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的 放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块, 产生冲突(evict),则采用最近最少使用策略 LFU 进行替换
在这里插入图片描述
图 7.9 物理内存的访问

7.6 hello进程fork时的内存映射
当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。加载并运行 hello 需要以下几个步骤:
1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2)映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
3)映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4)设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
在这里插入图片描述

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

7.8 缺页故障与缺页中断处理
缺页故障是一种常见的故障,当指令引用一个虚拟地址,在 MMU 中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。其处理流程遵循图 7.11 所示的故障处理流程。
在这里插入图片描述
图 7.11 故障处理流程
缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令再次发送 VA 到 MMU,这次 MMU 就能正常翻译 VA 了。
7.9动态存储分配管理
printf 函数会调用 malloc,下面简述动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块, 自动释放未使用的已经分配的块的过程叫做垃圾收集。
一、带边界标签的隐式空闲链表
1)堆及堆中内存块的组织结构:
在这里插入图片描述
在内存块中增加 4B 的 Header 和 4B 的 Footer,其中 Header 用于寻找下一个 blcok,Footer 用于寻找上一个 block。Footer 的设计是专门为了合并空闲块方便的。因为 Header 和 Footer 大小已知,所以我们利用 Header 和 Footer 中存放的块大小就可以寻找上下 block。
2)隐式链表所谓隐式空闲链表,对比于显式空闲链表,代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表,其中 Header 和 Footer 中的 block 大小间接起到了前驱、后继指针的作用。
3)空闲块合并因为有了 Footer,所以我们可以方便的对前面的空闲块进行合并。合并的 情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,我们只需要通过改变 Header 和 Footer 中的值就可以完成这一操作。
二、 显示空间链表基本原理
将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表, 在每个空闲块中,都包含一个 pred(前驱)和 succ(后继)指针,如下图:
在这里插入图片描述
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线 性时间减少到了空闲块数量的线性时间。
维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用 LIFO 的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比 LIFO 排序的首次适配有着更高的内存利用率,接近最佳适配的利用率
7.10本章小结
本章介绍了本章主要介绍了 hello 的存储器地址空间、intel 的段式管理、hello 的页式管理,以 intel Core7 在指定环境下介绍了 VA 到 PA 的变换、物理内存访问, 还介绍了 hello 进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。虽然 hello 很小,但无数个 hello 放在一起管理起来就变得非常棘手,计算机一定要有条理清晰的存储和访问机制才能保证访存的速度
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个 Liunx 文件就是一个 m 个字节的序列:B0,B1,…,Bm-1。所有的 I/O 设 备都被模型化为文件。
文件的类型有:
1.普通文件:包含任何数据,分两类
i.文本文件:只含有 ASCII 码或 Unicode 字符的文件
ii.二进制文件:所有其他文件
2.目录:包含一组链接的文件。每个链接都将一个文件名映射到一个文件
3.套接字:用于与另一个进程进行跨网络通信的文件
而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅的映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix I/O 接口的几种操作:
1.打开文件:程序要求内核打开文件,内核返回一个小的非负整数(描述符),用于标识这个文件。程序在只要记录这个描述符便能记录打开文件的所有信息。
2. shell 在进程的开始为其打开三个文件:标准输入、标准输出和标准错误。
3.改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置 k,初始为 0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行 seek 操作显式地设置文件的当前位置为 k。
4.读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k+n。给定一个大小为 m 字节的文件, 当 k>=m 时执行读操作会出发一个称为 EOF 的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的 EOF 符号。
5. 关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源, 并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O 函数:
1.int open(char *filename, int flags, mode_t mode);
open 函数将 filename 转换为一个文件描述符,并且返回描述符数字。 返回的描述符总是在进程中当前没有打开的最小描述符,flags 参数指 明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。
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);
write 函数从内存位置 buf 复制至多 n 个字节到描述符 fd 的当前文件位置。

8.3 printf的实现分析
前提:printf 和 vsprintf 代码是 windows 下的。
查看 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;
}

首先 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’: //只处理%x一种情况 itoa(tmp, ((int)p_next_arg)); //将输入参数值转化为字符串保存在tmp
strcpy(p, tmp); //将tmp字符串复制到p处 p_next_arg += 4; //下一个参数值地址
p += strlen(tmp); //放下一个参数值的地址 break;
case ‘s’:
break;
default:
break;
}
}
return (p - buf); //返回最后生成的字符串的长度
}
则知道 vsprintf 程序按照格式 fmt 结合参数 args 生成格式化之后的字符串,并 返回字串的长度。 在 printf 中调用系统函数 write(buf,i)将长度为 i 的 buf 输出。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_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 将字符串中的字节“Hello 1181910425 于春雨 2”从寄存器中通过总线复
制到显卡的显存中,显存中存储的是字符的 ASCII 码。
字符显示驱动子程序将通过 ASCII 码在字模库中找到点阵信息将点阵信息存 储到 vram 中。
显示芯片会按照一定的刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB 分量)。 于是我们的打印字符串“Hello 1181910425 于春雨”就显示在了屏幕上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。

8.5本章小结
本章介绍了 Linux 中 I/O 设备的管理方法,Unix I/O 接口和函数,并且分析了 printf 和 getchar 函数是如何通过 Unix I/O 函数实现其功能的。
(第8章1分)
结论
hello 程序终于完成了它“艰辛”的一生。hello 的一生大事记如下:
1.经过预处理器 cpp 的预处理,处理以#开头的行,得到 hello.i。
2.编译器 ccl 将得到的 hello.i 编译成汇编文件 hello.s。
3.汇编器 as 又将 hello.s 翻译成机器语言指令得到可重定位目标文件 hello.o
4. 链接器 ld 将 hello.o 与动态链接库链接生成可执行目标文件 hello,至 此,hello 自己已经脱胎换骨成了一个可以运行的程序。然而它的运行还要靠操作系统提供帮助。
5. 在 shell 中输入./hello 1181910425 于春雨 2,shell 为 hello fork 一个子进程,并在子进程中调用 execve,加载运行 hello。
6. CPU 为 hello 分配内存空间,hello 从磁盘被加载到内存。
7. 当 CPU 访问 hello 时,请求一个虚拟地址,MMU 把虚拟地址转换成物理地址并通过三级 cache 访存。
8. hello 运行过程中可能遇到各种信号,shell 为其提供了各种信号处理程序。
9. Unix I/O 帮助 hello 实现了输出到屏幕和从键盘输入的功能。
10. 最后 hello 执行 return 0;结束了自己的一生。
我的感想:
计算机系统真的是一个庞大而又精细的组织,即使是一个简单的 hello.c也需要操作系统提供如此多的支持,并且每一步都经过了设计者的深思熟虑,在有限的 硬件水平下把程序的时间和空间性能都做到了近乎完美,比如存储器的层次结构,比如虚拟内存的多级页表和 TLB……
在这里插入图片描述
(结论0分,缺失 -1分,根据内容酌情加分)

附件
文件名称 文件作用
hello.i 预处理之后文本文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标执行
hello 链接之后的可执行目标文件
helloo.objdmp Hello.o 的反汇编代码
helloo.elf Hello.o 的 ELF 格式
hello.objdmp Hello 的反汇编代码
hello.elf Hellode ELF 格式
(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
[7] ELF 构造:https://www.cs.stevens.edu/~jschauma/631/elf.html
[8] argc argv:https://baike.baidu.com/item/argc%20argv/10826112?fr=aladdin
[9] printf 函数实现的深入剖析:https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值