您的位置:首页 > 理论基础

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

2020-03-05 01:52 447 查看

计算机系统

大作业

题目: 程序人生-Hello’P2P
专业:计算机
学号:1170301009
班级:1803010
学生:王鹏远
指导教师:史先俊

计算机科学与技术学院
2019年12月
摘 要
本文通过本学期学习的计算机系统知识,在Linux系统下模拟hello.c程序P2P(From Program To Process)的过程。介绍了程序从最开始的预处理、编译、汇编、链接到形成目标文件,还包括进程管理、存储管理及IO设备的管理。通过重温一个c程序的一生来加深系统运行程序的理解。

关键词:预处理、编译、汇编、链接、进程管理、存储管理、IO管理

目 录

第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的P2P(From Program to Process):
首先我们通过键盘输入代码得到hello.c源程序文本。然后hello.c经过预处理器(cpp)形成hello.i文本文件,接着通过编译器(ccl)生成hello.s汇编语言文本文件,其次经过汇编器(as),成为hello.o可重定位目标程序(二进制),最终链接器(ld)处理合并,结果得到hello可执行目标程序(二进制)。执行该目标文件,操作系统会使用fork函数形成一个子进程,使用execve函数加载此进程。至此,hello由一个‘程序’变成了‘进程’。
Hello的020(From Zero-0 to Zero-0):
在hello运行的过程中。 程序对数据进行处理时,其空间在内存上申请。shell 为其映射虚拟内存,CPU访问相关数据需要MMU的虚拟地址到物理地址的转化,其中TLB和四级页表提高了地址翻译的速度。计算机的三级高速缓存结构以下一级作为上一级的缓存,让hello的数据能够从磁盘传输到寄存器。CPU为运行的hello分配时间片,执行逻辑控制流。操作系统将I/O设备都抽象为文件,让hello程序能够调用硬件进行从键盘读入字符,向屏幕输出内容的输入输出。最后shell负责回收hello进程,内核删除相关数据,释放其运行过程中占用的内存空间。
1.2 环境与工具
1.2.1 硬件环境
Intel Core i7-6700HQ x64CPU @2.60GHz;8.00GB;
1.2.2 软件环境
Windows 10; Ubuntu 18.04 ;
1.2.3 开发工具
Code:blocks;GCC;EDB;READELF;objdump;gedit;

1.3 中间结果
文件名 作用
hello.c hello源代码
hello.i 预处理文本文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位文件
hello 链接后的可执行文件
hello.txt hello.o的反汇编代码
helloelf.txt hello的ELF文本
helloobjdump.txt hello的反汇编代码
1.4 本章小结
本章介绍了Hello从源代码到执行的流程,包括预处理,编译,汇编,链接等过程。

第2章 预处理

2.1 预处理的概念与作用
预处理指在代码翻译的过程中生成二进制代码之前的过程。
预处理会展开以#起始的行解释为预处理指令
#if/#ifdef/#ifndef/#else/#elif/#endif、#define、#include等
include形式声明的文件复制到新的程序中,define定义的字符串用实际值代替。
预处理可以让目标程序变小,提高运行速度
2.2在Ubuntu下预处理的命令
gcc hello.c -E -o hello.i

经过预处理之后,hello.c文件转化为hello.i文件。下图为hello.i文件部分截图。

2.3 Hello的预处理结果解析
预处理之后的文件为3042行。原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中。从3029行开始为hello的main函数。

2.4 本章小结
本章展示了从hello.c到hello.i的过程和指令,以及生成的hello.i文件。从文件的篇幅我们可以看到预处理相较源代码增加了许多内容。

第3章 编译

3.1 编译的概念与作用
编译:1、利用编译程序从源语言编写的源程序产生目标程序的过程。 2、用编译程序产生目标程序的动作。编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。
编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
3.2 在Ubuntu下编译的命令
gcc -Og -S hello.i -o hello.s
在这里插入图片描述
3.3 Hello的编译结果解析

下面对hello.s进行具体的分析
3.3.1:堆栈的使用。我们看到前面一条指令便是push语句压入栈数据。因为%rbp和%rbx是被调用者保存的寄存器,因此要使用该寄存器首先要保存其状态以供恢复。之后申请栈空间存放变量。
3.3.2:参数的传递。由于是64位系统,因此参数传递采用寄存器。故argc、argv分别存放在rdi,rsi寄存器中。
3.3.3:关系操作。hello.c中需要将argc与4进行比较,对应到hello.s中的汇编代码便利用cmp语句加上跳转实现if的分支cmpl语句实现argc与4的比较。

3.3.4:控制转移。上面的汇编语句同样实现了if 的控制转移jne可实现带条件的跳转,实现控制转移,这些条件与标志寄存器有关。当然也有无条件跳转:jmp指令便可实现无条件控制转移。在编译器编译时循环指令并不会被优化为Loop的循环指令,而是利用条件转移实现的。

3.3.5:赋值操作。

通过leaq指令可以实现赋值,这里通过leaq指令计算赋给寄存器的值是一个地址,用以实现函数调用。除此之外,还可通过mov指令实现赋值操作。如下的mov指令都是赋值操作。mov指令是非常常见的数据传送指令,可以实现赋值操作。

3.3.6:函数调用。通过call指令实现函数调用。如下call语句对应着C程序中诸如printf,exit,getchar,sleep函数的调用的汇编实现。

函数的返回结果都保存在寄存器%eax(%rax)中。
3.3.7:全局变量。

.L6中首条指令用leaq指令得到的地址便是一个全局变量的地址。全局变量的地址可通过基于rip的相对寻址得到。这个全局变量便对应着argc!=4时要打印的字符串。
3.3.8:局部变量。局部变量用寄存器来实现。如本源程序中的局部变量i,便是用%ebx来实现。

这一指令便对应着i的初始值为0.
3.3.9:指针/数组操作。由于argv是指针数组,每一个元素都是8字节指针,因此可利用带比例因子的相对基址变址的寻址方式实现。

3.3.10:++操作。C程序中每一次循环都会对循环变量i进行++操作,但在编译器优化时也未使用简单的INC指令,而是由add指令来完成。
3.3.11:函数返回。利用ret指令即可。但在返回时要进行堆栈平衡,同时函数的返回值保存在%eax(%rax)中。

3.4 本章小结
本阶段完成了对hello.i扩展文件的编译。在Ubuntu下使用编译指令即可完成对.i文件的编译。本章重点做了汇编代码的解析工作。

第4章 汇编

4.1 汇编的概念与作用
概念:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o 中。
作用:汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。
4.2 在Ubuntu下汇编的命令
gcc -Og -c hello.s -o hello.o

4.3 可重定位目标elf格式
ELF头如下:

各节信息如下:

可重定位条目:


.rela.text是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改,调用本地函数的指令则不需要修改。
offset表示需要进行重定向的代码在.text或.data节中的偏移位置,Type表示着该符号的重定义类型:是相对地址引用还是绝对地址引用。Addend是重定位时需要的偏移调整。
4.4 Hello.o的结果解析
使用objdump -d -r hello.o > helloo.txt分析hello.o的反汇编,并与第3章的hello.s进行对照分析。
发现有以下几点不同——
1.函数调用:原来的函数名字被替换成了函数的相对偏移地址
2.跳转语句:由原来的段地址变成了相对偏移地址。
3.全局变量:由使用段名称+%rip,变成了在反汇编代码中0+%rip。这是因为 rodata中数据地址也需要重定位。
4.操作数:由hello.s里面的十进制,变成了在反汇编代码中的十六进制。
汇编语言以及机器码的对应:
一条指令就是机器语言的一个语句,是一组有意义的二进制代码。指令的基本格式:操作码字段和地址码字段,其中操作码指明了指令的操作性质及功能,地址码则给出了操作数或操作数的地址。
4.5 本章小结
本章介绍了从hello.s到hello.o的汇编过程,首先理清了汇编的概念和作用,知道了Linux下如何汇编以及反汇编。查看hello.o的可重定位目标elf格式,并通过使用objdump得到反汇编代码与hello.s进行比较。

第5章 链接

5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用:链接利用重定位信息将各种代码和数据包括库函数重新组合,生成可执行文件。同时,编译也使得分离编译成为可能。利用链接我们可以将一个大型的应用程序分解为更小的便于管理的模块,独立修改和编译这些小模块,最终链接即可。
5.2 在Ubuntu下链接的命令
gcc -m64 -no-pie -fno-PIC hello.o -o hello

5.3 可执行目标文件hello的格式


其中各个条目包括了各段的基本信息:size标识了该段的大小,address标识了该段的起始地址,offset标识了该段相对于文件的起始的偏移量。
5.4 hello的虚拟地址空间
使用edb加载查看hello:
从Data Dump窗口可观察到各个地址的详细内容:

对应5.3中内容,虚拟地址空间从0x400000开始,到0x401000结束,在窗口中已经给出标识。可以详细去对应5.3中的每一段进行查看:
这里列出5.3中部分段在Data Dump中的对应:

0x400200至0x40021c为.interp段。

0x400550至0x400664为.text段。

0x400670至0x4006b0为.rodata段。

5.5 链接的重定位过程分析
使用objdump -d -r hello得到反汇编结果。可以明显发现该结果与hello.o的反汇编结果不同。可执行文件的反汇编结果中给出了重定位结果,即虚拟地址的确定。而hello.o的反汇编结果中,各部分的开始地址均为0。

5.6 hello的执行流程
调用与跳转的各个子程序名及程序地址如下:子程序名程序地址
<ld-2.27.so!_dl_start>0x7fcb305feea0
<ld-2.27.so!_dl_init>0x7fcb3060d630
<hello!_start>0x400550
<libc-2.27.so!__libc_start_main>0x7fcb3022dab0
<libc-2.27.so!__cxa_atexit>0x7fcb3024f430
<hello!__libc_csu_init>0x4005f0
<libc-2.27.so!_setjmp>0x7fcb3024ac10
<hello!main>0x400582
<hello!puts>0x4004f0
<hello!exit>0x400530
5.7 Hello的动态链接分析
在dl_init之前各个寄存器的值如下图

在dl_init之后各个寄存器的值如下图:

5.8 本章小结
本阶段完成了对hello.o的链接工作。使用Ubuntu下的链接指令可以将其转换为.out可执行目标文件。此外,本章通过一系列细致分析,了解了链接过程中的具体细节。完成该阶段转换后,即得到了可以执行的二进制文件了。

第6章 hello进程管理

6.1 进程的概念与作用
概念:进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。

作用:进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell的作用:Shell是一个用C语言编写的程序,他是用户使用Linux的桥梁。Shell 是指一种应用程序,Shell应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序为其分配子进程并运行
5)shell应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
在shell的main函数中可通过fork函数创建一个新的子进程,这个新创建的子进程拥有着与父进程的虚拟地址空间以及打开的文件相同但独立的一份副本,二者仅有PID不同。子进程保留有父进程在调用fork函数时的状态,此后父进程与子进程便成为并发的进程,执行顺序不定。fork函数在两个进程中有着不同的返回值,在父进程中返回子进程的PID,在子进程中返回0,由此便可区分二者,并利用此当作判断条件,选定子进程,令其利用execve函数来执行hello程序。于是,这样便利用fork函数创建了一个可以执行hello的子进程。
6.4 Hello的execve过程
execve的函数原型为:int execve(char *filename,char *argv[],char *envp[])
execve函数加载并运行可执行目标文件filename,argv是其参数列表,envp是环境变量列表。令hello作为其第一个参数,使其可以运行hello程序。执行时其会删除当前子进程现有的虚拟内存段,创建一组新的段,其中新的代码段与数据段初始化为可执行文件hello的内容,实现覆盖(但还保留相同的PID),堆与栈初始化为0,同时将虚拟空间中的页映射到可执行文件对应内容实现页分配。之后调用启动代码,启动代码设置栈,将控制传递给hello程序的主函数,之后在具体执行hello的指令时由于未映射到物理内存会触发缺页中断并通过处理程序加载运行
6.5 Hello的进程执行
进程的执行并不一定是完整、连续地执行完成,每一个进程在执行时都会有其对应的时间片。如hello程序在开始被操作系统调度执行时会分配一个大小固定的时间片,当时间片的时间用尽后或者被其他信号(如硬件异常)中断,如果hello程序此时还未执行完成,内核也可以决定抢占当前进程,重新恢复一个之前被抢占的进程如hello2,实现进程的调度。如hello与hello2两个进程,开始时hello进程在用户模式下执行,操作系统为其分配了固定大小的时间片,时间片用尽后hello程序还未执行完毕,但控制仍然转移给操作系统进入到内核模式,此时由内核选择执行哪一个进程,这个选择称作进程调度。这里假设操作系统调度进程hello2,之后将hello的上下文信息保存,恢复hello2的上下文信息,将控制转移给hello2,之后再次进入用户模式在固定时间片内执行进程hello2.之后重复上述过程至hello与hello2进程终止。
6.6 hello的异常与信号处理
分析如下:
首先带参数运行hello程序。如图:

首先尝试不停按回车:这属于硬件异常,进程正常执行,并没有任何反应,如图:可见默认处理行为为忽略。

接着我们尝试按下Ctrl+C,这是来0自键盘的中断,会产生SIGINT信号,对该信号的默认处理行为是终止前台进程。

接着尝试Ctrl+Z,这属于来自终端的停止信号,会产生SIGTSTP信号,对该信号的默认处理行为是挂起前台进程,按下Ctrl+Z时便以提醒进程已停止,如图:

6.7本章小结
在本章中,阐明了进程的定义与作用,介绍了Shell的一般处理流程,调用fork创建新进程,调用execve执行hello,hello的进程执行,hello的异常与信号处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间
①物理地址是用于内存芯片的单元寻址,与处理器和CPU连接的地址总线相对应。即地址总线能够切实寻址的地址,与物理内存一一对应,对应到hello程序来说就是hello在真正被执行时机器访问用地址总线访问的hello的地址
②逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址可以表示为[段选择符:偏移地址]。对应到hello程序来说,你可以说某个变量的地址是xxx,这就是它的逻辑地址,是它相对于hello进程的数据段的偏移地址,与物理地址相干。但在Intel实模式下逻辑地址CS:EA =>物理地址CS16+EA有这样的转换关系。
③虚拟地址指有程序产生的由段选择符和段内偏移地址组成的地址,是一种抽象,我们看到的hello的地址如0x400582等都是虚拟地址,每个进程都会有这样一个独立的虚拟地址空间,访问时先给出逻辑地址,需要再转换为虚拟地址,再通过虚拟地址到物理地址的映射转换为物理地址实现寻址。
④线性地址指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间中的地址。程序代码会产生逻辑地址,也就是段中的偏移地址,加上相应的段基址就成了线性地址。可表示为[段描述符:段偏移]形式。如果开启了分页机制,那么线性地址需要再经过变换,转为为物理地址。如果无分页机制,那么线性地址就是物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址是由一个段标识符加上一个指定段内相对地址的偏移量构成,表示为[段标识符:段内偏移量]。
首先看段选择符的T1 = 0还是1,知道当前要转换的时是全局段描述符表中的段还是局部段描述符表中的段,再根据相应寄存器得到其地址和大小
拿出段选择符中前13位的索引在相应的数组中查找到对应的段描述符,这样就找到了它的基地址。
基地址 + 段内偏移量就是要转换的线性地址。

7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址被分成了3部分,由低位到高位依次为VPO、TLB索引、TLB标记。其中TLB是位于MMU中的关于PTE的缓存,称为翻译后被缓冲器。
进行地址翻译时,现根据TLB索引及标记找到相应PPN,若标记位的有效位为0,则直接到PTE中去找,将PPN与VPO串联起来,就得到了相应的物理地址。

7.4 TLB与四级页表支持下的VA到PA的变换
图7-2给出了Core i7MMU如何使用四级的页表来将虚拟地址翻译成物理地址。36位的vn被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。vn1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。vn2提供到一个L2 PTE的偏移量,以此类推。

7.5 三级Cache支持下的物理内存访问
首先CPU生成一个虚拟地址,经过地址翻译后将其转化为物理地址,然后根据物理地址的各字段访问L1 Cache请求数据,若发生不命中,则向下一级缓存中请求数据(L2 ->L3->主存)。

7.6 hello进程fork时的内存映射
内核为了给新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面标记为只读,并将进程中的每个区域结构都标记为私有的写时复制。
当fork函数在新进程中返回时,新进程现在的虚拟内存刚好和调用fork是存在的的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。有几个步骤:
1.删除已存在的用户区域:删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制0的,映射到匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制0的,初始长度为0.
3.映射共享区域:与hello程序链接的共享对象动态链接到程序,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器:使PC指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页中断就是要访问的页不在主存,需要操作系统将其调入主存后再进行访问。在这个时候,被内存映射的文件实际上成了一个分页交换文件。
缺页中断处理:
如图7-5所示,CPU引用VP3的一个字,地址翻译硬件从内存读取PTE3,从有效位判断出VP3为被缓存,触发缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在本例中为存放在PP3中的VP4。

接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3。随后返回

7.9动态存储分配管理
分配器将堆视为一组不同大小的块 (blocks)的集合来维护,每个块要么是已分配的,要么是空闲的。
记录空闲块:隐式空闲链表,显示空闲链表,红黑树等
放置以分配的块:
首次适配:从链表开始寻找适合的空堆块,直到找到为止
下一次适配:从上一次的分配点开始找,直到找到适合的空堆块为止
最佳适配:查询整个链表,找到最合适(浪费最小)的空堆块
块的分割:当前的空堆块大小大于所需要大小且剩余部分若满足最小要求,则将块分两为两部分
获取额外的堆存储器:
当链表中不能满足申请要求的堆块空间的时候,1)通过合并相邻的堆块空间,形成单个尽量大的堆块空间 2)实在没有其他办法了,分配器通过sbrk函数向内核申请格外的堆空间,分配器将堆空间插入到链表中,然后提供给申请空间的块。
7.10本章小结
本章主要借助hello这一具体进程介绍了Unix的存储管理,包括段式管理、页式管理,有介绍了地址翻译与访存,并简单介绍了C语言中的显示动态内存分配。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法
设备的模型化:文件
类型:普通类型 目录 套接字
设备管理:unix io接口
所有的I/O设备都被模型化为文件,而所有的输入输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
IO接口及其函数
Unix IO接口:
1.打开文件:一个应用程序通过要求内核打开相应文件来宣告他想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件内核记录有关这个文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。
3.改变当前文件的位置。对于每个打开的文件,内核保持这一个文件位置k,初始为0。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。
函数:
1.打开文件
int
open(char *filename , int flags , mode_t mode);
open函数将filename转换成一个文件描述符,并且返回描述符数字,出错则返回-1
参数flags指明了进程打算如何访问这个文件,也可以是一个或者更多位掩码的或,为写提供一些额外的指示
参数mode指定了新文件的访问权限位
2.关闭文件
int
close(int fd);
成功返回0,出错返回-1
3.读文件
ssize_t
read(int fd , void *buf , size_t n);
返回:成功则为读的字节数,若EOF则为0,若出错为-1
4.写文件
ssize_t write(int fd , const void *buf ,
size_t n);
返回:若成功则为写的字节数,出错则为-1
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;
}
函数的定义过程中使用了可变参数。
其中,fmt是指向字符的指针,指向第一个参数,这个参数是固定的,可以通过这个参数的位置及C语言函数参数入栈的特点来引用其他可变参数。
vsprintf函数返回打印字符串的长度,它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
之后调用write(buf,i),查看write汇编代码
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
找到INT_VECTOR_SYS_CALL的实现:
init_idt_desc(INT_VECTOR_SYS_CALL,DA_386IGate,
sys_call, PRIVILEGE_USER);
通过系统调用了sys_call,猜测就是这个函数驱动了显示器,在看它的代码:
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
最终将格式化了的字符串显示在屏幕上。以看出代码里面的call是访问字库模板并且获取每一个点的RGB信息最后放入到eax也就是输出返回的应该是显示vram的值,然后系统显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析
当程序调用getchar时.运行程序时就等着用户从按键输入,用户输入的字符被存放在键盘缓冲区中.直到用户按回车为止(回车字符也放在缓冲区中),当用户键入回车之后,getchar才开始从输入流中每次读入一个字符,输入的字符不只一个的时候,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完之后,才等待用户按键,getchar函数输入数字也按字符处理,单个的getchar函数输入多于一个字符时,只接收第一个字符.
8.5本章小结
Linux系统下,所有的I/O设备都被模型化为文件,而所有的输入输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,即Unix I/O,这使得所有的输入输出都能以一种统一且一致的方式来执行。

结论

1.hello.c 通过I/O 键入,存入主存。
2.利用预处理器,对.c文件预处理,生成hello.i文件
3.利用编译器,将文本文件hello.i翻译成包含一个汇编语言程序的文本文件hello.s
4.利用汇编器将hello.s翻译成机器语言指令,生成了可重定位目标文件hello.o
5.利用链接器将系统的库与hello.o链接,生成可执行目标文件hello
6.在命令行中键入“./hello”,shell调用fork函数为hello创建子进程
7.shell调用execve函数创造一个新进程即将hello安排进去
8.CPU为hello分配时间片,在内核的调度下,hello在自己的上下文中运行自己的逻辑流。
9.对于一些异常和中断信号hello可以正常处理
10.hello运行结束后,shell会对其进行回收,内核删除在创建hello进程时建立的记录。

附件

hello.c hello源代码
hello.i 预处理文本文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位文件
hello 链接后的可执行文件

参考文献

[1]Csapp《深入理解计算机系统》
[2]printf函数实现的深入剖析 https://blog.csdn.net/ztguang/article/details/51011779
[3]虚拟地址、逻辑地址、线性地址、物理地址
https://blog.csdn.net/rabbit_in_android/article/details/49976101
[4]printf的实现分析 :https://www.cnblogs.com/pianist/p/3315801.html
[5]TLB的那些事儿 https://blog.csdn.net/omnispace/article/details/61415935

  • 点赞
  • 收藏
  • 分享
  • 文章举报
剑雪封喉· 发布了1 篇原创文章 · 获赞 0 · 访问量 12 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: