Hello的一生

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

Hello的P2P: hello.c经预处理器生成hello.i,再经编译器生成hello.s,再经汇编器生成hello.o,最后被链接器链接生成目标程序(program)hello。在shell中,利用fork创建子进程,再用execve加载hello程序,于是hello就由程序(program)变成了一个进程(process)。
Hello的020:shell从调用hello之前,到hello运行结束退出的过程。shell先使用fork函数和execve函数创建子进程、加载并运行hello,等待hello运行结束以后,父进程(shell)再去回收这个子进程,最后shell又变成了与调用hello之前相同的状态。
1.2 环境与工具
硬件环境:X64 CPU;2.90GHz;12G RAM;256GHD Disk;
软件环境:Windows 10;Vmware 11;Ubuntu 16.04 LTS 64位;
开发工具:CodeBlocks;vi/vim/gpedit+gcc
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.i hello.c经预处理后生成的文件 进行了编译预处理,进行了宏替换、头文件加载和条件编译
hello.s hello.i经编译后生成的汇编文件 将文件翻译成了汇编语言
hello.o hello.s经汇编后生成的可重定位目标文件 将文件翻译成了机器语言
hello 经链接后生成的最终文件
hello.o.txt hello.o的反汇编文本
hello.out.txt hello的反汇编文本
hello.elf 使用readelf生成的elf文件

1.4 本章小结
本章讲述了Hello的一生。从hello.c被编写出来,再经预处理、编译、汇编、链接生成可执行文件Hello,然后被shell运行,最后被系统回收,结束它的一生。原本看似简单的一个程序,却有着大多数程序都有的历程(program to process、zero to zero)。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理概念:在编译之前进行的处理,修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。 C语言的预处理主要有三个方面的内容: 1.宏定义; 2.文件包含; 3.条件编译。 预处理命令以符号“#”开头。

预处理作用:
1.进行宏替换 #define 标识符 文本 用文本替换标识符
2.文件包含 #include<文件名> 将文件或系统头文件的内容插入文件
3.条件编译 #if / #ifdef / #endif 在条件满足时才编译

2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i

(图2-2 编译预处理指令)

2.3 Hello的预处理结果解析

(图2-3-1 预处理结果1)

(图2-3-2 预处理结果2)
在main函数前添加了近3000行代码(如图2-3-2)。预处理器在预处理时读取系统头文件stdio.h、unistd.h、stdlib.h的内容,并将它们插入文本中。此外,注释等对程序无用的信息也被删除。
2.4 本章小结
本章介绍了预处理的概念、作用和Ubuntu下的预处理指令。并结合hello.c经预处理后生成的hello.i文件进行解析。预处理只是将需要的内容全部放入一个文件,并没有开始编译、生成可执行文件。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:将某一种程序设计语言写的程序翻译成等价的另一种语言的程序
作用:
将预处理后的文件hello.i翻译成汇编语言,生成hello.s
1.词法分析
2.语法分析
3.生成目标代码
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s

(图3-2 编译指令)
3.3 Hello的编译结果解析

(图3-3 编译结果)

3.3.1 数据类型

数据类型:整型、字符型、字符指针数组、字符串

全局变量int sleepsecs:
sleepsecs先在.text节中声明为全局变量,然后在.data节中声明。.data节存放已初始化的全局变量和静态变量,因此sleepsecs在.data节。

局部变量int i:
局部变量存在寄存器或栈上,i存在栈中。

传入参数int argc
第一个参数存在寄存器%edi中

传入参数*argv[],第二个传入参数
通过利用
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
获得argv[1]的地址

两个字符串存在.rodata节。

3.3.2 各类操作

3.3.2.1赋值:

int sleepsecs=2.5;
i=0;
sleepsecs在.data节中完成声明、赋值
i利用movl $0, -4(%rbp)进行赋值

3.3.2.2类型转换:

int sleepsecs=2.5;
将float类型隐式转换为int类型,转换时向0舍入,因此有sleepsecs: .long 2
sleepsecs的值为2

3.3.2.3算数操作:

i++;
hello.s中使用addl $1, -4(%rbp)对i进行自增。i存放在%rbp-4处,每次执行加一

3.3.2.4关系操作:

argc!=3
i<10;
hello.s中以cmpl $3, -20(%rbp)来实现与argc与3的比较,并设置条件码,如果不等则继续执行接下来的代码,相等则执行下一条je .L2。
以cmpl $9, -4(%rbp)来比较i与10的大小并设置条件码,如果小于等于9则执行jle .L4,否则执行接下来的代码

3.3.2.5数组/指针/结构操作:

指针数组char *argv[]
argv[1],argv[2]
通过利用
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
获得argv[1]的地址
通过利用
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
获得argv[2]的值
汇编语言中通过加括号来实现指针的解引用

3.3.2.6控制转移:

if(argc!=3)
hello.s通过cmpl $3, -20(%rbp) je .L2来实现。首先比较argc与3的值并设置条件码,然后je判断如果条件码ZF为0,则说明argc等于3,则跳转到L2处,否则执行下一条语句。

3.3.2.7函数操作:

main()
printf();
exit(1);
sleep();
return 0;

main函数:系统通过调用call来执行,并将下一条指令的地址压入栈中。在执行时先push %rbp保存旧栈指针。参数argc、agrv分别用%rdi、%rsi存储。可在运行过程中调用其他函数,调用时要将下一条指令的地址压入栈中。执行到return 0时返回,汇编指令为leave ret,从栈中恢复旧栈指针到%rbp,%rax变为0,返回到压入栈中的下一条指令的地址。

printf函数:
先把要输出的字符串地址存入%edi,然后call printf。hello.s中第一次调用时把“Hello 学号 姓名”字符串的地址.LC0存入%edi然后调用puts。第二次调用时把“Hello %s %s”字符串的地址.LC1存入%edi然后调用printf。

exit()
hello.c中使用了exit(1),因此hello.s中用movl $1, %edi让%edi存第一个参数1,然后用call exit调用exit函数

sleep()
hello.s通过movl sleepsecs(%rip), %eax movl %eax, %edi将参数sleepsecs存入%edi然后call sleep

3.4 本章小结
本章介绍了编译的概念、作用与ubuntu下的编译命令,并对hello.s对照hello.c进行解析,分析c语言中的数据类型与各类操作在汇编语言中的实现。其中汇编语言是直接面向处理器的程序设计语言,每种处理器都有自己可识别的一套指令,称为指令集。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编是指将hello.s汇编程序用汇编器生成hello.o重定位程序的过程。汇编的作用是将汇编语言翻译成机器可以读懂、执行的命令。汇编程序使用的是汇编语言,重定位文件使用的是二进制数:0、1。

4.2 在Ubuntu下汇编的命令
gcc hello.s -o hello.o

(图4-2 汇编指令)
4.3 可重定位目标elf格式
ELF头(图4-3-1):以一个16字节的序列Magic开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量。

(图4-3-1 elf头)

节头部表(图4-3-2):文件中出现的各个节的类型、位置和大小等信息

(图4-3-2 节头部表)

重定位节(图4-3-3):一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。包含偏移量、信息、类型、符号值等信息。

(图4-3-3 重定位节)
程序头、段节:
(图4-3-4 图4)

4.4 Hello.o的结果解析

(图4-4-1) (图4-4-2)

(图4-4-1为hello.s,图4-4-2为hello.o)
hello.s与hello.o中的汇编指令格式基本相同,但是生成的hello.o不再使用jmp .L3 je .L2这类跳转而是变成了je 40066f这样直接跳转到确定的地址,且使用的是相对寻址,原来的.L2、.L3中的指令也被合并到了main函数的里边, 因此.L2、.L3之类的段也就不存在了。函数调用时也不是再使用call+函数名,而是call+地址,这些地址的值目前都是下一条指令的地址,只有在链接后才能变成真正的地址。而访问全局变量时,hello.s用的是movl sleepsecs(%rip), %eax,而hello.o用的是mov 0x2009b2(%rip),%eax。同时其他存储在栈上的数据相对rsp的偏移量也有了变化。hello.s用的是汇编语言,而hello.o用的是相应汇编语言翻译成的机器语言(二进制)。

4.5 本章小结
本章介绍了汇编的概念、作用与ubuntu下的汇编命令,同时查看了elf可重定位目标文件的组成,并将hello.o与hello.s进行比较,得知hello.o是相应汇编语言在进行了一定调整后转换成的机器语言,其中每一条汇编语言指令都能翻译成相应的二进制代码。了解了从汇编语言到机器语言的转化。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
5.2 在Ubuntu下链接的命令
gcc -c -o hello.o hello.c
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-2 链接指令)
5.3 可执行目标文件hello的格式
hello的ELF中节头声明了各节的信息,其中包括各节的大小、类型、地址、偏移量(如图5-3-1~5-3-4)。可根据地址、偏移量和大小来确定各节所存储在的区域。各节的地址使用的是虚拟地址。

(图5-3-1 ELF各段信息1-ELF头)

(图5-3-2 ELF各段信息2-节头)

(图5-3-3 ELF各段信息3)

(图5-3-4 ELF各段信息4)

5.4 hello的虚拟地址空间
在edb中可查看到程序的虚拟地址是从0x400000开始,到0x400ff0结束的。各节对应的虚拟地址与ELF中节头里各节的地址相同,说明ELF用的都是虚拟地址而不是物理地址。

(图5-4-1 虚拟地址空间)

(图5-4-2 edb查看虚拟地址空间)
5.5 链接的重定位过程分析
hello相比与hello.o增加了
section .init:
<_init>
section .plt:
puts@plt-0x10
puts@plt
printf@plt
__libc_start_main@plt
getchar@plt
exit@plt
sleep@plt
.plt.got:
<.plt.got>
且hello.o中的起始地址是从0x0开始,而hello是从0x400000。在链接时,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一起进行链接,链接器将程序中调用的函数(printf、put、exit、sleep等)从相应的文件中加载到hello里,而不加载这些文件中的其他代码。在链接过程中,程序进行了重定位,在函数调用时,计算了被调用函数与当前调用指令的相对偏移量,并将地址改为相对地址。所有的call指令之后的地址都被重新计算,从而指向相应函数。对字符串的引用则是直接使用相应地址(.rodata节地址加相对位置)而不再是0。

(图5-5-1 hello.o与hello对比)

(图5-5-2 hello.o与hello对比)
5.6 hello的执行流程

ld-2.23.so!_dl_start
ld-2.23.so!_dl_init
hello!_start
libc-2.23.so!__libc_start_main@plt
libc-2.23.so!__cxa_atexit
libc-2.23.so!__libc_csu_init
hello!_init
libc-2.23.so!_setjmp
libc-2.23.so!_sigsetjmp
libc-2.23.so!__sigjmp_save
hello!main
hello!puts@plt
hello!exit@plt
*hello!printf@plt
*hello!sleep@plt
*hello!getchar@plt
ld-2.23.so!_dl_runtime_resolve_xsave
ld-2.23.so!_dl_fixup
ld-2.23.so!_dl_lookup_symbol_x
libc-2.23.so!exit

(图5-6 执行流程)
5.7 Hello的动态链接分析

_GLOBAL_OFFSET_TABLE_的变化
调用前:

(图5-7-1 调用dl_init前)
调用后:

(图5-7-2 调用dl_init后)
5.8 本章小结
本章介绍了链接的概念与作用、Ubuntu下链接的指令(不能只链接hello.o这一个文件)、生成的可执行文件hello的虚拟地址空间、hello的执行流程、链接的重定位过程与动态链接分析。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中的程序的实例。每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时,shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

6.2 简述壳Shell-bash的作用与处理流程
作用:shell 是一个交互型应用级程序,它代表用户运行其他程序。通过收到用户在stdin的输入,根据特定输入来执行不同的操作,用户通过控制命令行的输入,控制要执行的操作
处理流程:读入命令——shell分析输入字符串参数——shell检测是否是内置命令——是,则执行内置命令——不是,则调用相应应用,分配子进程运行。
6.3 Hello的fork进程创建过程
shell通过使用fork函数来创建子进程,新创建的子进程具有与父进程相同的虚拟地址空间、代码和数据段、堆、共享栈及用户栈的副本,但是PID不同。在输入运行hello程序的命令后,shell开始进行命令解析,判断出hello不是内置命令后,shell通过调用fork函数创建子进程来运行hello程序。

(图6-3 fork创建过程)
6.4 Hello的execve过程
shell在使用fork创建子进程之后,子进程使用execve函数加载并运行相应程序且加载成功后不会返回,只有找不到需要加载的程序时才会返回-1。在运行hello时,shell使用fork创建子进程后,子进程使用execve函数加载并运行hello。其中fork调用一次返回两次;execve调用一次,成功后不返回。

(图6-4 execve过程)

6.5 Hello的进程执行
控制流通过上下文切换从一个进程传递到另一个进程。分配给某个进程的时间就叫做进程的时间片。用户模式中的进程不允许执行特权指令例如发起I/O操作、停止处理器等,而内核模式可以。运行应用程序代码的进程初始时是在用户模式中。进程从用户模式变为内核模式的唯一方法是通过中断、故障或陷入系统调用这类异常。当异常发生时,处理器将异常传递到异常处理程序,变为内核模式。处理程序返回时变回用户模式。
在运行Hello时,Hello首先运行在用户模式中,当调用sleep函数后,进入内核模式。内核模式中,内核将Hello加入等待队列,计时器开始计时,内核上下文切换至用户模式运行另一个进程。当计时器达到2s后发出中断信号,内核进入内核模式,然后上下文切换进入用户模式运行Hello。

(图6-5 hello进程执行)

6.6 hello的异常与信号处理

来自键盘的中断 SIGINT 默认行为:终止
子进程停止或终止 SIGCHLD 默认行为:忽略
来自终端的停止信号 SIGSTP 停止直到下一个SIGCONT
继续进程 SIGCONT 继续进程如果该进程停止
杀死程序 SIGKILL 默认行为:终止

正常运行:

(图6-6-1 程序正常运行)

乱按:

(图6-6-2 乱按键盘)

Ctrl-C:

(图6-6-3 输入Ctrl-C)

Ctrl-Z:

(图6-6-4 输入Ctrl-Z)

Ctrl-Z后ps:

(图6-6-5 输入Ctrl-Z后ps)

Ctrl-Z后jobs:

(图6-6-6 输入Ctrl-Z后jobs)

Ctrl-Z后pstree:

(图6-6-7 输入Ctrl-Z后pstree)

Ctrl-Z后fg:

(图6-6-8 输入Ctrl-Z后fg)

Ctrl-Z后kill:

(图6-6-9 输入Ctrl-Z后kill)

其中按下Ctrl-C为发送SIGINT信号,Ctrl-Z为发送SIGSTP信号,fg为发送SIGCONT信号,kill为发送SIGKILL信号。
6.7本章小结
本章介绍了进程的概念与作用、shell的处理流程与Hello从运行到终止的全部过程。其中包括了fork函数创建子进程,execve函数加载程序,发送信号引起异常与信号处理。

(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是指由程序产生的与段相关的偏移地址部分。对于hello就是hello.o里面的相对偏移地址。
线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。就是hello里面的虚拟内存地址。
虚拟地址:CPU启动保护模式后,程序运行在虚拟地址空间中。在这里等价于线性地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。是hello在运行时虚拟内存地址经变换得到的地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是指把一个程序分成若干个段进行存储,每个段都是一个逻辑实体。它的产生是与程序的模块化直接有关的。段式管理是通过段表进行的,它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。
一个逻辑地址由两部份组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节

(图7-2-1 段选择符)
索引号就是“段描述符(segment descriptor)”,段描述符具体地址描述了一个段。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,每一个段描述符由8个字节组成。

(图7-2-1 段描述符)

一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。

具体的转化步骤:
1.给定一个完整的逻辑地址。
2.看段选择符T1,知道要转换的是GDT中的段还是LDT中的段,通过寄存器得到地址和大小。
3.取段选择符中的13位,再数组中查找对应的段描述符,得到BASE,就是基地址。
4.线性地址等于基地址加偏移。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。

(图7-3-1 基于页表的地址翻译)

(图7-3-2 基于k级页表的地址翻译)

转化步骤:
1.从cr3中取出目录地址。
2.根据线性地址前m位(VPN 1),在数组中,找到对应索引项,因为引入多级管理,所以页目录中的项,不再是页的地址,而是一个页表的地址。
3.根据之后的VPN找到下一级页表直到VPN k,然后在第k级页表中找到页的起始地址。
4.将页的起始地址与线性地址中的VPO相加,得到物理地址。

7.4 TLB与四级页表支持下的VA到PA的变换
首先CPU产生虚拟地址,然后将虚拟地址分为前36位VPN和后12位VPO。其中VPN可分为前32位TLBT和后4位TLBI来作为TLB的标记与索引,从而向TLB进行匹配。如果TLB命中,则直接从TLB中取出PPN。如果不命中,则由CR获得一级页表地址,然后根据VPN找到下一级页表,直到第四级页表,最终得到PPN。最后将VPO作为PPO然后与PPN结合,得出物理地址

(图7-4 基于四级页表的地址变换)
7.5 三级Cache支持下的物理内存访问

首先获得物理地址VA,然后将前40位PPN作为CT,后12位PPO作为6位CI和6位CO,然后以CI和CT作为索引和标记与L1 cache进行匹配,若匹配成功且块的有效位为1,则命中,然后根据CO取出数据。若不命中,则向下一级cache中寻找,寻找方式同L1 cache。最后将数据缓存(若有空闲块则直接放置,否则选择一个块驱逐)

(图7-5 物理内存访问)

7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。并且创建hello进程的mm_struct 、vm_area_struct和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。在hello返回时,hello拥有与调用fork进程时相同的虚拟内存。随后的写操作通过写时复制机制创建新页面。
7.7 hello进程execve时的内存映射
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

(图7-7 映射地址空间)

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

缺页:虚拟内存中的字不在物理内存中 (DRAM 缓存不命中)
缺页异常:缺页导致页面出错
缺页处理:缺页异常处理程序选择一个牺牲页,如果这个牺牲页面被修改过,那么就将它交换出去,然后换入新的页面并更新页表。之后导致缺页的指令重新启动。

(图7-8 缺页异常示意)

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap) 。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) 。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。
分配器将堆视为一组不同大小的块 (blocks)的集合来维护,每个块要么是已分配的,要么是空闲的。分配器主要分为显式分配器和隐式分配器。
策略:显式空闲链表和隐式空闲链表。隐式空闲链表:通过头部中的大小字段隐含的连接。分配器可以通过遍历堆中的所有块,从而间接地遍历整个空闲块地集合。可添加脚部从而实现双向链表。寻找空闲块时可使用首次适配、下一次适配和最佳适配;分配空闲块时进行分割;释放块时需要按照四种情况合并相邻空闲块。

(图7-9 隐式空闲链表)

添加脚部的隐式空闲链表:

(图7-9-2 添加脚部的隐式空闲链表)

7.10本章小结

本章主要介绍了Hello的存储器地址空间、逻辑地址到线性地址的变换——段式管理、线性地址到物理地址的变换——页式管理、在TLB和页表支持下虚拟地址转化为物理地址、在cache支持下的物理内存访问、Hello进程运行时fork和execve的内存映射、缺页故障与处理、动态存储分配管理。主要围绕数据的存储和存储器层次结构展开。

(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法

设备的模型化:文件
设备管理:unix io接口
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这就是Unix I/O接口。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,通知它想要访间一个I/O 设备。内核返回一个小的非负整数,它在后续对此文件的所有操作中标识这个文件。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。

Unix I/O函数:
1.进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:
int open(char *filename, int flags, mode_t mode);
open 函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。
返回:若成功则为新文件描述符,若出错为-1。
2.进程通过调用close 函数关闭一个打开的文件。
int close(int fd);
返回:若成功则为0, 若出错则为-1。
3.应用程序是通过分别调用read 和write 函数来执行输入和输出的。
ssize_t read(int fd, void *buf, size_t n);
read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf 。返回值-1表示一个错误,而返回值0 表示EOF。否则,返回值表示的是实际传送的字节数量。
返回:若成功则为读的字节数,若EOF 则为0, 若出错为-1。
ssize_t write(int fd, const void *buf, size_t n);
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。
返回:若成功则为写的字节数,若出错则为-1。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了linux的IO设备管理方法、Unix的IO接口及其函数、printf函数的实现和getchar函数的实现

(第8章1分)
结论
对于一般人而言,hello从被编写到编译、运行、结束不过是一段很短暂的时间,对于刚学编程的人来说也是几分钟后就抛弃掉。然而hello的一生却隐含着许多复杂而又对大多数程序必不可少的过程。
1.编写。首先,它被用高级语言(c语言)编写出来,形成hello.c;
2.预处理。hello.c就被预处理器进行编译预处理,将引用的外部文件合并进来生成hello.i;
3.编译。hello.i被编译器翻译成汇编语言,生成hello.s;
4.汇编。汇编器将hello.s中的汇编语言翻译成机器语言,生成hello.o;
5.链接。链接器将hello.o与其它需要的可重定向目标文件链接,生成可执行程序hello;
6.运行。在shell中输入运行可执行文件hello的指令(./hello 1172700102 刘明瑄)。
7.创建子进程。shell用fork函数创建子进程。
8.运行程序。shell在子进程中使用execve函数加载并运行hello。
9.执行指令。hello在CPU为其分配的时间片内执行自己的逻辑控制流。
10.访问内存。MMU要先将程序中的虚拟内存地址翻译成物理地址。
11.动态内存申请。printf会调用malloc函数。
12.信号的接收、处理。在运行过程中收到来自键盘的输入要根据信号做出相应的反应,调用相应的信号处理程序。
13.运行结束。shell父进程回收子进程。
hello的一生就此结束。
只有学习了计算机系统这门课,才能知道hello.c中这短短的几行代码以及编译器上的“编译并运行”按钮的背后是多么的复杂,而且还远不止上述的复杂程度……

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

附件
列出所有的中间产物的文件名,并予以说明起作用。

hello.i hello.c经预处理后生成的文件 进行了编译预处理,进行了宏替换、头文件加载和条件编译
hello.s hello.i经编译后生成的汇编文件 将文件翻译成了汇编语言
hello.o hello.s经汇编后生成的可重定位目标文件 将文件翻译成了机器语言
hello 经链接后生成的最终文件
hello.o.txt hello.o的反汇编文本
hello.out.txt hello的反汇编文本
hello.elf 使用readelf生成的elf文件

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

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 兰德尔E.布莱恩特 大卫R.奥哈拉伦. 深入理解计算机系统(第3版).
机械工业出版社. 2018.4.
[2] 百度百科
[3] 计算机系统 课件
[4] LINUX逻辑地址、线性地址、物理地址和虚拟地址
https://www.cnblogs.com/zengkefu/p/5452792.html
[5] printf函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值