计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 软件工程
学 号 1173710104
班 级 1737101
学 生 滕涛
指 导 教 师 吴锐
计算机科学与技术学院
2018年12月
摘 要
hello.c是每一个程序员所写出的人生中的第一个程序,但是不要看hello.c简短,其在计算机中运行却有着许许多多的奥妙值得每一个程序员去探索,由一个简单的程序开始去一步一步了解更加复杂且值得去探索的程序,才会得到一步一步的成长,这次的大作业就从hello开始第一步。
关键词:CSAPP;Hello程序;生命周期;
(摘要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简介
在Editor中键入代码得到hello.c程序,在linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork,fork产生一个shell进程的子进程,于是Program变成Process,这是P2P(From Program to Process)的过程。之后shell为其execve,加载器为其分配内存空间,程序运行在物理内存中,CPU 为其分配时间片执行指令。当程序运行结束后,shell父进程负责回收hello进程,这便是020 (From Zero-0 to Zero -0)的过程。
1.2 环境与工具
硬件环境:Intel Core i7-7700HQ x64CPU,8G RAM,256G SSD +1T HDD. 软件环境:Ubuntu18.04.1 LTS
开发与调试工具:文字编辑器,gcc,as,ld,edb,readelf,HexEdit
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名称 文件作用
hello.c 源代码
hello.i 预处理之后文本文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标执行
hello 链接之后的可执行目标文件
hello2.c 测试程序代码
hello2 测试程序
helloo.objdmp Hello.o的反汇编代码
helloo.elf Hello.o的ELF格式
hello.objdmp Hello的反汇编代码
hello.elf Hellode ELF格式
hmp.txt 存放临时数据
1.4 本章小结
本章主要简单介绍了hello的p2p,020过程,列出了本次实验信息:环境、中间结果。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器cpp根据以字符#开头的命令,修改原始的C程序。主要功能如下:
2.3 Hello的预处理结果解析
因为 hello.c 包含的头文件中还包含有其他头文件,因此系统会递归式的寻址和展开,直到文件中不含宏定义且相关的头文件均已被引入。故使用文字编辑器打开hello.i后发现main函数之前出现的是stdio.h , unistd.h和stdlib.h,以stdio.h的展开为例,cpp到默认的环境变量下寻找stdio.h,打开/usr/include/stdio.h 发现其中依然使用了#define 语句,cpp 对此递归展开,所以最终.i 程序中是没有#define 的。而且发现其中使用了大量的#ifdef #ifndef的语句,cpp 会对条件值进行判断来决定是否执行包含其中的逻辑。其他类似。
2.4 本章小结
本章主要介绍了预处理的定义与作用、并结合预处理之后的程序对预处理结果
进行了解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是利用编译程序从源语言编写的源程序产生目标程序的过程,也是用编译程序产生目标程序的动作。编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。这个过程称为编译,同时也是编译的作用。
编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1 汇编指令
指令 含义
.file 声明源文件
.text 声明代码段
.data 声明数据段
.section .rodata 以下是rodata节
.globl 声明一个全局变量
.type 声明指令及数据存放地址的对齐方式
.size 声明大小
.long、.string 声明一个long、string类型
.align 声明对指令或者数据的存放地址进行对齐的
方式
3.3.2 数据
一、字符串
汇编语言中,输出字符串作为全局变量保存,因此存储于. rodata 节中。汇编文件 hello.s 中,共有两个字符串,均作为 printf 参数,分别为:
1) “Usage: Hello 学号 姓名!\n”,第一个printf传入的输出格式化参数,在hello.s中声明,可以发现字符串被编码成UTF-8格式,一个汉字在utf-8编码中占三个字节,一个\代表一个字节。
2)“Hello %s %s\n”,第二个printf传入的输出格式化参数,在hello.s中声明
二、整型
在 hello.s文件中,sleepsecs 的定义如下:
程序中涉及的整数数据有:
1) int sleepsecs:sleepsecs 在 C 程序中被声明为全局变量,且已经被赋值,编译器处理时在.data节声明该变量,.data节存放已经初始化的全局和静态 C 变量。在图 3.3 中,可以看到,编译器首先将 sleepsecs在.text 代码段中将其声明为全局变量,其次在.data段中,设置对齐方式为 4、设置类型为对象、设置大小为 4 字节、设置为 long 类型其值为2。
2) int i:编译器将其值存储在寄存器或者栈空间中,两者选择具体决定于寄存器的数目以及优化程度,在 hello.s 编译器将 i 存储在栈上空间-4(%rbp)中,可以看出i占据了栈中的4B大小空间。编译器 ccl 会将 int 表示为 long 但对齐方式仍为 int 型的 4 字节,long 类型表示为双字 quad,对齐方式仍为 8 字节。
3) int argc:作为第一个参数传入
4) 立即数:其他整形数据的出现都是以立即数的形式出现的,直接硬编码在汇编代码中。
三、数组
在hello.c中运用数组如下
此部分循环对应汇编代码段为
argv[]为char类型单个数组元素的大小为8个字节起始地址为argv,传入的两个参数分别记为argv[1],argv[2],值为getchar()在函数运行时获取的两个%s,后通过指令取出了数组储存值。
3.3.3 赋值
hello.c文件中赋值操作共有两次:⑴int sleepsecs = 2.5 ⑵int i = 0
⑵ sleepsecs被定义为全局变量被定义为long类型,值为2
⑵ i的赋值通过mov操作来完成,即movl $0,-4(%rbp)
对于mov操作来说可分为movb(8位)、movw(16位)、movl(32位)、movq(64位)
3.3.4 类型转换
程序中涉及隐式类型转换的是:int sleepsecs=2.5,将浮点数类型的2.5强制转换为int类型。
当在double或float向int进行强制类型转换的时候,程序改变数值和位模式的
原则是:值会向零舍入。例如 1.999 将被转换成 1,-1.999 将被转换成-1。进一步
来讲,可能会产生值溢出的情况,与 Intel兼容的微处理器指定位模式[10…000]为整数不确定值,一个浮点数到整数的转换,如果不能为该浮点数找到一个合适的整数近似值,就会产生一个整数不确定值。
浮点数默认类型为double,所以上述强制转化是double强制转化为int类型。遵从向零舍入的原则,将2.5舍入为2。
3.3.5 算数操作
算数操作的汇编指令有
指令 效果
leaq S,D D=&S
INC D D+=1
DEC D D-=1
NEG D D=-D
ADD S,D D=D+S
SUB S,D D=D-S
IMULQ S R[%rdx]:R[%rax]=SR[%rax](有符号)
MULQ S R[%rdx]:R[%rax]=SR[%rax](无符号)
IDIVQ S
R[%rdx]=R[%rdx]:R[%rax] mod S(有符号)R[%rax]=R[%rdx]:R[%rax] div S
DIVQ S R[%rdx]=R[%rdx]:R[%rax] mod S(无符号)
R[%rax]=R[%rdx]:R[%rax] div S
hello.s中的算数操作有:
⑵ i++ 在for循环中i累加,在hello.s中的汇编操作为addl $1,-4(%rbp)
⑵leaq指令中地址%rip被加上了.LC0,.LC1并分别传给了%rdi进行操作
leaq .LC0(%rip),%rdi 和 leaq .LC1(%rip),%rdi
3.3.6 关系操作
进行关系操作的汇编指令有:
指令 效果 描述
CMP S1,S2 S2-S1 比较-设置条件码
TEST S1,S2 S1&S2 测试-设置条件码
SET** D D=** 按照将条件码设置D
J —— 根据**与条件码进行跳转
程序中涉及的关系运算为:
1)argc!=3:判断argc不等于3。hello.s中使用cmpl $3,-20(%rbp)比较,计算
argc-3然后设置条件码,为下一步je利用条件码进行跳转作准备。
2)i<10:判断i小于10。hello.s中使用cmpl $9,-4(%rbp)比较,计算i-9 然后
设置条件码,为下一步jle使用条件码进行跳转做准备。
3.3.7 控制转移
指令 语法 功能
JN\JE… JN & 跳转到地址
hello.s中的控制转移有:
⑴ if (argv!=3):当argv不等于 3 的时候执行程序段中的代码。对标志位ZF进行判断,ZF=1即相等时跳转到.L2
⑵ for(i=0;i<10;i++)运行时先无条件跳转到位于循环体.L4之后的比较代码,使用cmpl进行比较,如果i<=9,则跳入.L4 for循环体执行,否则说明循环结束,顺序执行for之后的逻辑。
3.3.8 函数操作
函数是一种过程,过程提供了一种封装代码的方式,用一组指定的参数和可选的返回值实现某种功能。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函数:
① 参数传递:将int argc, char argv[]传入main函数。
② 函数调用:main函数调用了printf(),exit(),sleep(),getchar()四个函数
③ 函数返回:当main函数正常运行结束后返回1
(2) printf函数:
① 传递数据:第一次printf将%rdi设置为“Usage: Hello 学号 姓名! \n”字符串的首地址。第二次printf设置%rdi 为“Hello %s %s\n” 的首地址,设置%rsi为argv[1],%rdx 为argv[2]。
② 控制传递:第一次 printf 因为只有一个字符串参数,所以 call puts@PLT;第二次printf使用call printf@PLT。
(3) exit函数:
① 参数传递:传入了1作为参数。
② 函数返回:exit(1)表示程序出现异常时退出。
(4) sleep函数:
① 参数传递:传入了sleepsecs
② 函数返回:传入了sleepsecs之后,程序在传参大小时间过后继续进行
(5) getchar函数:
① 控制传递:call gethcar@PLT
② 函数返回:函数返回输入字符的ASCII码或者EOF表示输入有误
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
本章系统阐述了编译器将预处理文本文件 hello.i 翻译为文本文件 hello.s 的具体操作,主要就汇编语言伪指令、数据类型、汇编语言操作、控制转移,函数操作、类型转换六方面针对 hello.s 中各部分做出相应的解释说明。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:把汇编语言翻译成机器语言的过程称为汇编。
作用:汇编器(as)将 hello.s 翻译成机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在二进制目标文件 hello.o 中。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
应截图,展示汇编过程!
4.3 可重定位目标elf格式
首先使用 readelf –a hello.o > hello.elf 生成 hello.o 文件的 ELF 格式。分析其组成各部分:
a) ELF 头
ELF 头以一个 16 字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括 ELF 头的大小、目标文件的类型(如可重定位、可执行或共享的)、机器类型(如 x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的头目(entry)。hello.elf 的 ELF 头如下:
a) 节头部表
节头部表记录了各节名称、类型、地址、偏移量、大小、全体大小、旗标、连接、信息、对齐信息。hello.elf 节头部表如下:
b) 重定位节
.rela.text 记录了一个. text 节中位置的列表,当链接器把这个目标文件和其他文件组合时需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。
在 hello.o 的重定位节中包含了 main 函数调用的 puts、exit、printf、sleep、getchar 函数以及全局变量 sleepsecs,还有. rodata 节(包含 prnitf 的格式串)的偏移量、信息、类型、符号值、符号名称及加数。rela.eh_frame 记录了. text 的信息。hello.elf 的重定位节如下:
c) 符号表
用来存放程序中定义和引用的函数和全局变量的信息。重定位需要引用的符号都在其中声明。
4.4 Hello.o的结果解析
利用 objdump -d -r hello.o > hello_o_asm.txt 生成 hello.o 对应的反汇编文件,经过与 hello.s 比较,二者却产生了细微的差别。如上图所示。
主要原因在于:
⑴ 分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L3,而段名称只是在汇编语言中便于编写的助记符,在汇编成机器语言之后变成了确定的地址。
⑵ 函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。
⑶ 全局变量访问:在.s文件中,访问rodata(printf中的字符串),使用段名称+%rip,在反汇编代码中0+%rip,因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
4.5 本章小结
本章通过对汇编后产生的 hello.o 的可重定位的 ELF 格式的考察、对重定位项目的举例分析以及对反汇编文件与 hello.s 的对比,从原理层次了解了汇编这一过程实现的变化。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合称为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用:链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。早期计算机系统中链接时手动执行的,在现代系统中,链接器由链接器自动执行。链接器使得分离编译成为可能。开发过程中无需将大型的应用程序组织委员一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。
注意:这儿的链接是指从 hello.o 到hello生成过程。
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_out.elf 执行获得包含 hello 的 ELF 格式的文件。
节头部表中包含了各段的基本信息,包括名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐等信息:
5.4 hello的虚拟地址空间
用 edb 打开 hello,可以在 Data Dump 窗口看见 hello 加载到虚拟地址中的状况,程序的虚拟地址空间为 0x00000000004000000-0x0000000000401000,如下图:
查看 .elf 中的程序头部分
elf 里面的 Program Headers:
PHDR:程序头表
INTERP:程序执行前需要调用的解释器
LOAD:程序目标代码和常量信息
DYNAMIC:动态链接器所使用的信息
NOTE::辅助信息
GNU_EH_FRAME:保存异常信息
GNU_STACK:使用系统栈所需要的权限信息
GNU_RELRO:保存在重定位之后只读信息的位置
5.5 链接的重定位过程分析
使用objdump -d -r hello > hello.objdump 获得hello的反汇编代码。
与 hello.o 反汇编文本 hello.objdump 相比,在 hello.objdump 中多了许多节
节名称 描述
.interp 保存ld.so的路径
.hash 符号的哈希表
.gnu.hash GNU拓展的符号的哈希表
.dynsym 运行时/动态符号表
.dynstr 存放.dynsym节中的符号名称
.gnu.version 符号版本
.gnu.version_r 符号引用版本
.rela.dyn 运行时/动态重定位表
.rela.plt .plt节的重定位条目
.init 程序初始化需要执行的代码
.plt 动态链接-过程链接表
.fini 当程序正常终止时需要执行的代码
.eh_frame contains exception unwinding and
source language information. Each entry
in this section is represented by single
CFI
.dynamic 存放被ld.so使用的动态链接信息
.got 动态链接-全局偏移量表-存放变量
.got.plt 动态链接-全局偏移量表-存放函数
.data 初始化了的数据
.comment 一串包含编译器的 NULL-terminated 字
符串
函数调用
hello.o 反汇编文件中,call 地址后为占位符(4 个字节的 0);而 hello 在生成过程中使用了动态链接共享库,函数调用时用到了延时绑定机制。以 puts 为例,简述其链接过程:
数据访问 hello.o 反汇编文件中,对. rodata 中 printf 的格式串的访问需要通过链接时重定位的绝对引用确定地址,因此在汇编代码相应位置仍为占位符表示,对. data 中已初始化的全局变量 sleepsecs 为 0x0+%rip 的方式访问;而 hello 反汇编文件中对应全局变量已通过重定位绝对引用被替换为固定地址。
5.6 hello的执行流程
加载程序 ld-2.23.so!_dl_start
ld-2.23.so!_dl_init
LinkAddress!_start
ld-2.23.so!_libc_start_main
ld-2.23.so!_cxa_atexit
LinkAddress!_libc_csu.init
ld-2.23.so!_setjmp
运行 LinkAddress!main
程序终止 ld-2.23.so!exit
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章了解了链接的概念作用,分析可执行文件 hello 的 ELF 格式及其虚拟地址空间,同时通过实例分析了重定位过程、加载以及运行时函数调用顺序以及动态链接过程,深入理解链接和重定位的过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量、以及打开文件描述符的集合。
作用:进程给应用程序提供的关键抽象有两种:
a) 一个独立的逻辑控制流,提供一个假象,程序独占地使用处理器。
b) 一个私有的地址空间,提供一个假象,程序在独占地使用系统内存。
6.2 简述壳Shell-bash的作用与处理流程
Shell的作用:Shell是一个用C语言编写的程序,他是用户使用 Linux 的桥梁。Shell既是一种命令语言,又是一种程序设计语言,Shell 是指一种应用程序。Shell
应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序执行
5)shell应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
进程的创建过程:父进程通过调用 fork 函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用 fork 时。子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程最大的区别在于他们有不同的 id。
fork 后调用一次返回两次,在父进程中 fork 会返回子进程的 PID,在子进程中 fork 会返回 0;父进程与子进程是并发运行的独立进程。内核能够以任何方式交替执行他们逻辑控制流中的指令。
hello 的 fork 进程创建过程为:系统进程创建 hello 子进程然后调用 waitpid() 函数知道 hello 子进程结束
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。函数声明如下:
int execve(const char *filename, const char *argv[], const char *envp[]);
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。正常情况下,execve调用一次,但从不返回。
从栈底(高地址)到栈顶(低地址),首先是参数和环境字符串。栈往上紧随其后的是以null结尾的指针数组,其中每个指针都指向栈中的一个环境变量字符串。全局变量environ指向这些之阵中的第一个envp[0]。紧随环境变量数组之后的是以null结尾的argv[]数组,其中每个元素都指向栈中的一个参数字符串。在栈的顶部是系统启动函数libc_start_main的栈帧。
程序运行fork后,子进程调用exceve函数在当前进程的上下文中加载并运行一个新程序即 hello 程序,execve 调用驻留在内存中的被称为启动加载器的操作系统代码来执行 hello 程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置 PC 指向_start 地址,_start 最终调用 hello中的 main 函数。
6.5 Hello的进程执行
逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流
使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占
(暂时挂起),然后轮到其他进程。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄
存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,
用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的
代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任
何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由
通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内
核数据结构等对象的值构成。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这个决策就叫做调度,是由内核中称为调度器的代码处理的。在内和调度了一个新的进程运行后,它就抢占当前进程,并使用上文所述的上下文切换的机制将控制转移到新的进程。内核代表的用户执行系统调用时,可能会发生上下文切换;中断也有可能引发上下文切换。
通过内核模式用户模式的切换描述用户态核心态转换的过程,在切换的第一部分中,内核代表进程 A 在内核模式下执行指令。然后在某一时刻,它开始代表进程 B(仍然是内核模式下) 执行指令。在切换之后,内核代表进程 B 在用户模式下执行指令。随后,进程 B 在用户模式下运行一会儿,直到磁盘发出一个中断信号,表示数据已经从磁盘传送到了内存。内核判定进程 B 已经运行了足够长的时间,就执行一个从进程 B 到进程 A 的上下文切换,将控制返回给进程 A 中紧随在系统调用 read 之后的那条指令。进程 A 继续运行,直到下一次异常发生,依此类推。
6.6 hello的异常与信号处理
查看hello.c文件进行,首先,如果参数不为3,那么会打印一条默认语句,并异常退出。如果参数是3个,那么会执行一个循环,每次循环会使hello进程休眠2.5秒,休眠后又会恢复hello。而且循环里会输出一条格式字串,其中有输入的两个参数字串。循环结束后,有一个getchar()等待一个标准输入,然后就结束了。
6.6.1正常运行
程序正产运行,运行结束后进程被回收
6.6.2不停乱按
在运行时随意键入,输入的字符串被保存在了标准输入的缓冲区内在得到’\n’指令时,被认为是指令,在hello执行完后被输入到终端中执行。
6.6.3 Ctrl+C
shell父进程收到SIGINT信号,结束hello,并回收hello进程
6.6.4 Ctrl+Z
输出三条hello信息后键入Ctrl+Z信号,父进程收到信号后,将hello进程挂起,查看可发现hello进程并未被回收,调用fg 后再次被调到前台执行,输出余下的字符串。
jobs 命令可以查看当前的关键命令(ctrl+Z/ctrl+C 这类)内容,比如这时候就会返回 ctrl+Z 表示暂停命令
pstree 是用进程树的方法把各个进程用树状图的方式连接起来
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
本章从进程的角度分别描述了 hello 子进程 fork 和 execve 过程,并针对 execve 过程中虚拟内存映像以及栈组织结构等作出说明。同时了解了逻辑控制流中内核的调度及上下文切换等机制。阐述了 Shell 和 Bash 运行的处理流程以及 hello 执行过程中可能引发的异常和信号处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:又称相对地址,是程序运行由 CPU 产生的与段相关的偏移地址部分。他是描述一个程序运行段的地址。
物理地址:程序运行时加载到内存地址寄存器中的地址,内存单元的真正地址。他是在前端总线上传输的而且是唯一的。在 hello 程序中,他就表示了这个程序运行时的一条确切的指令在内存地址上的具体哪一块进行执行。
线性地址:这个和虚拟地址是同一个东西,是经过段机制转化之后用于描述程序分页信息的地址。他是对程序运行区块的一个抽象映射。
虚拟地址:是Windows程序时运行在386保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理(segmentation),是指把一个程序分成若干个段(segment)进行存储,每个段都是一个逻辑实体(logical entity)。
用户栈是栈段寄存器,共享库的内存映射区域和运行时堆都是辅助段寄存器,读/写段是数据段寄存器,只读代码段是代码段寄存器。
段寄存器(16位)用于存放段选择符:
CS(代码段):程序代码所在段
SS(栈段):栈区所在段
DS(数据段):全局静态数据区所在段
其他3个段寄存器ES、GS和FS可指向任意数据段
段选择符各字段含义:
CS寄存器中的RPL字段表示CPU的当前特权级(Current Privilege Level,CPL) RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位于最低级的用户态,第0级高于第3级。出于环保护机制,内核工作在第0环,用户工作在第3环,中间环留给中间软件用。Linux仅用第0环和第3环。TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)。
段描述符是一种数据结构,实际上就是段表项,分为用户的代码段和数据段描述符,还有系统控制端描述符。
全局描述符表GDT:只有一个,用来存放系统内每个任务都可能访问的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及TSS(任务状态段)等都属于GDT中描述的段、局部描述符表LDT:存放某任务(即用户进程)专用的描述符
中断描述符表IDT:包含256个中断门、陷阱门和任务门描述符
7.3 Hello的线性地址到物理地址的变换-页式管理
概念上而言, 虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。 每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘(较低层)上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。 VM系统通过将虚拟内存分割为称为虚 拟页(VirtualPage, VP)的大小固定的块来处理这个问题。 每个虚拟页的大小为P= 护字节。 类似地,物理内存被分割为物理页(Physical Page, PP) , 大小也为P字节(物理页也被称为页帧(page frame) )。
为了有助于清晰理解存储层次结构中不同的缓存概念,我们将使用术语SRAM缓存 来表示位于CPU和主存之间的Ll、L2和L3高速缓存,并且用术语DRAM缓存来表示虚拟内存系统的缓存,它在主存中缓存虚拟页。
页表是一种数据结构,它用于计算机操作系统中的虚拟内存系统,其存储了虚拟地址到物理地址间的映射。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。最简单的分页表系统通常维护一个帧表和一个分页表。帧表处理帧映射信息。更高级系统中,帧表可以处理页属于的地址空间,统计信息和其他背景信息。分页表处理页的虚拟地址和物理帧的映射。还有一些辅助信息,如当前存在标识位(present bit),脏数据标识位或已修改的标识位,地址空间或进程ID信息。
页表就是一个页表条目(PageTable Entry, PTE)的数组。虚拟地址空间中的每个页在页表中一固定偏移最处都有一个PTE。假设每个PTE是由一个有效位(validbit)和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。 如果设置了有效位,那么地址字段就表示DRAM中相应的 物理页的起始位置, 这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。
形式上来说,地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素 的物理地址空间(PAS)中元素之间的映射。
上图展示了MMU如何利用页表来实现这种映射。CPU中的一个控制寄存器,页表基址寄存器(PageTable Base Register, PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位虚拟页面偏移(VirtualPage Offset, VPO)和一个(n-p) 位的虚拟页号(Virtual age Number, VPN)。MMU利用VPN来选择适当的PTE。例如,VPN0选择PTEO, VPN 1选择PTE1, 以此类推。将页表条目中物理页号(PhysicalPage Number, PPN)和虚拟 地址中的VPO串联起来,就得到相应的物理地址。注意,因为物理和虚拟页面都是P字节的,所以物理页面偏移(PhysicalPage Offset, PPO)和VPO是相同的。
7.4 TLB与四级页表支持下的VA到PA的变换
在 Intel Core i7环境下研究VA到PA的地址翻译问题。前提如下:
虚拟地址空间48位,物理地址空间52位,页表大小4KB,4级页表。TLB 4 路16组相联。CR3指向第一级页表的起始位置(上下文一部分)。
解析前提条件:由一个页表大小4KB,一个PTE条目8B,共512个条目,使
用9位二进制索引,一共4个页表共使用36位二进制索引,所以VPN共36位,因为VA 48位,所以 VPO 12位;因为TLB共 16组,所以TLBI需4 位,因为VPN
36位,所以TLBT 32 位。
如图 ,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 三级Cache支持下的物理内存访问
在此仅讨论L1级缓存情况,由于L2,L3级缓存访问过程与L1相似。
物理内存访问,是基于MMU将虚拟地址翻译成物理地址之后,向cache中访问的。
在cache中物理地址寻址,按照三个步骤:组选择、行匹配和字选择。在冲突不命中时还会发生行替换。
高速缓存(S, E, B, m)被组织成一个有S=2s个高速缓存组(cache set)的数组。 每个组包含E 个高速缓存行(cache line).每个行是由一个B=2b字节的数据块(block)组成的, 一个有效位(valid bit) 指明这个行是否包含有意义的信息,还有t=m-(b+s)个标记位(tag bit)(是当前块的内存地址的位的一个子集),它们唯一地标识存储在这个高速缓存行中的块。
一般而言,高速缓存的结构可以用元组(S, E, B, m)来描述。 高速缓存的大小(或容量)C指的是所有块的大小的和。标记位和有效位不包括在内。因此,C=S×E×B。
高速缓存的结构将m个地址位划分为t个标记位,s个组索引位,和b个块偏移位。
在组选择中,cache按照物理地址的s个组索引位(S=2s)来定位该地址映射的组。
选择好组后,遍历组中的每一行,比较行的标记和地址的标记,当且仅当这两者相同,并且行的有效位设为1时,才可以说这一行中包含着地址的一个副本。也就是缓存命中了。
最后是字选择。定位好了要寻址的地址在哪一行之后,根据地址的块偏移量,在行的数据块中偏移寻址,最后得到的字,就是我们寻址得到的字。
如果缓存不命中,那么它需要从存储器层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位指示的组中的一个高速缓存行中。这个过程,如果有冲突不命中,就会触发行的替换。
7.6 hello进程fork时的内存映射
当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给
它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve 函数代用驻留在内核区域的启动加载器代码,在当前进程中加载并运
行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区 域,称为堆(heap)系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做"break"),它指向堆的顶部。
分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块 保持空闲,直到它显式地被应用所分配。一个已分配 的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格。 两种风格都要求应用显式地分配块。 它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器(explicitallocator), 要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做ma耳oc程序包的显式分配器。C程序通过调用malloc函数来 分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
隐式分配器(implicitallocator) , 另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbagecollector), 而自动释放未使用的巳分配的块的过程叫做垃圾收集(garbagecollection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
⑴ 隐式空闲链表—在此介绍带边界标签的隐式空闲链表分配器原理
假设想要释放的块为当前块。那么合并下一个空闲块很简单而且高效。当前块的头部指向下一个块的头部,可以检查这个指针以判断下一个块是否是空闲的。如果是,就将它的大小简单地加到当前块头部的大小上,这两个块在常数时间内被合并。
给定一个带头的隐式空闲链表,唯一的选择将是搜索整个链表。记住前面块的位置,直到我们打到当前块。使用隐式空闲链表,这意味着每次调用free需要的时间都于堆的大小呈线性关系。即使使用更复杂精细的空闲链表组织,搜索时间也不会是常数。
Knuth提出一种聪明而通用的技术,叫做边界标记,允许在常数时间内进行对前面块的合并,这种思想,是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如下所示:
如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当期块开始位置一个字的距离。那么,分配器释放当前块时存在四种可能情况:
(1) 前面的块和后面的块都是已分配的
(2) 前面的块是已分配的,后面的块是空闲的
(3) 前面的块是空闲的,而后面的块是已分配的
(4) 前面的和后面的块都是空闲的。
下图,展示了这四种情况合并的过程:
⑵ 显示空闲链表
显式空闲链表是一种更好的方式是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是常数,这取决于我们所选择的空闲链表中块的排序策略。
一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块位置放在链表的开始出。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块。在这种情况下,释放一个块可以在常数时间内完成。如果使用了边界标记,那么合并也可以在常数时间内完成。
另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序的首次适配比LIFO排序的首次适配有更高的存储器利用率,接近最佳适配的利用率。
7.10本章小结
本章从 Linux 存储器的地址空间起,阐述了 Intel 的段式管理和页式管理机制,以及 TLB 与多级页表支持下的 VA 到 PA 的转换,同时对 cache 支持下的物理内存访问做了说明。针对内存映射及管理,简述了 hello 的 fork 和 execve 内存映射,了解了缺页故障与缺页中断处理程序,对动态分配管理做了系统阐述。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的IO设备都被模型化为文件,而所有的输入和输出都被
当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
Unix I/O接口统一操作:
设备可以通过Unix I/O接口被映射为文件,这使得所有的输入和输出都能以一种统一且一致的方式来执行:
⑴ 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
⑵ Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件
⑶ 改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置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接口函数:
⑴ 在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);
fd是需要关闭的文件的描述符,close返回操作结果。成功返回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的实现分析
printf函数是在stdio.h头文件中声明的,具体代码实现如下:
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;
}
它的参数包括一个字串fmt,和…。…表示参数个数不能确定,也就是格式化标准输出,我们也不能确定到底有几个格式串。
首先arg获得第二个不定长参数,即输出的时候格式化串对应的值。
va_list是一个数据类型,其声明如下:
typedef char va_list
赋值语句右侧的,是一个地址偏移定位,定位到了从fmt开始之后的第一个char变量,也就是第二个参数了。
接下来是调用vsprintf函数,并把返回值赋给整型变量i。后来又调用write函数从内存位置buf处复制i个字节到标准输出。想必这个i就是printf需要的输出字符总数,那么vsprintf就是对参数解析了。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++) { //p初始化为buf,下面即将把fmt解析并把结果存入buf中
/* 寻找格式化字串 */
if (*fmt != '%') {
*p++ = *fmt;
continue;
}
fmt++; //此时,fmt指向的是格式化字串的内容了
switch (*fmt) {
/*这是格式化字串为%x的情况*/
case 'x':
itoa(tmp, *((int*)p_next_arg)); //把fmt对应的那个参数字串转换格式,放到tmp串中
strcpy(p, tmp); //tmp串存到p中,也就是buf中
p_next_arg += 4; //定位到下一个参数
p += strlen(tmp); //buf中的指针也要往下走
break;
/* Case %s */
case 's':
break;
default:
break;
}
}
return (p - buf);
}
则知道vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
在printf中调用系统函数write(buf,i)将长度为i的buf输出。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 学号 姓名”从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
于是字符串“Hello 学号 姓名”就显示在了屏幕上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键
的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
getchar函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在
键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章节讲述了一下 linux 的 I/O 设备管理机制,了解了开、关、读、写、转移文件的接口及相关函数,简单分析了 printf 和 getchar 函数的实现方法以及操作过程。
(第8章1分)
结论
本次大作业,围绕着hello程序的一次运行展开了理解和讨论,在完成的过程中,也是对自己在这一学期所学知识的一个小的总结。
回顾 hello 短暂的一生,可以看出 hello 虽然很简单,但是却凝结着人类的智慧。从编写,到预处理,到编译、链接,再到运行,执行,访问内存,申请动太内存和信号处理,最后终止。
而这些,只是一个程序猿的开始。
(结论0分,缺少 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
文件名字 文件作用
hello.i 预处理产生文件
hello.s 编译产生文件
hello.o 汇编产生文件
hello 链接产生可执行文件
hello.elf hello.o的elf文件
hello_out.elf 包含 hello 的elf格式的文件
hello_o_asm.txt hello.o 对应的反汇编文件
(附件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.
(参考文献0分,确实 -1分)