哈尔滨工业大学计算机系统大作业——程序人生

家电修理 2023-07-16 19:17www.caominkang.com电器维修

哈尔滨工业大学

 

计算机系统

大作业

                                                      题目程序人生-Hello’s P2P 

                                                      专业计算机

                                                      学号120L021026

                                                      班级2003009

                                                      学生熊雄

                                                      指导教师郑贵滨

摘要

       本文主要论述了hello.cC语言源文件经过预处理、汇编、编译、链接,生成可执行文件的过程,通过分析hello程序从代码编辑器到运行进程的过程,分析和介绍计算机系统编译源文件、运行进程等机制,较为系统的描述hello的生命周期。

关键词计算机系统;编译;Linux;C语言;预处理;链接;   

                  

目录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

6.2 简述壳Shell-bash的作用与处理流程

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

7.2 Intel逻辑地址到线性地址的变换-段式管理

7.3 Hello的线性地址到物理地址的变换-页式管理

7.4 TLB与四级页表支持下的VA到PA的变换

7.5 三级Cache支持下的物理内存访问

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献

第1章 概述

1.1 Hello简介

P2P将C语言源程序hello.c经过预处理cpp生成hello.i文件,通过编译器l生成汇编程序hello.s,之后生成可重定位目标程序hello.o,ld链接生成可执行目标程序hello。在shell中输入命令后,shell为其fork产生一个子进程,实现由程序到进程的转变。

020在上述子进程终用execve运行,通过虚拟内存映射将程序从磁盘载入物理内存中执行,分配时间片,进行取指、译码、执行。对hello进行内存映射,开始运行,从物理内存中取出代码与数据,通过IO在屏幕上输出信息,父进程回收hello进程,释放hello的内存并删除进程上下文,结束hello的一生,实现从Zero到Zero的转变。

1.2 环境与工具

硬件环境X64 CPU;2GHz;2G RAM;256GHD Disk以上

软件环境Windos10 64位以上;VirtualBox 11以上;Ubuntu 16.04 LTS 64位以上;

开发工具Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+g

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.c源程序hello.i预处理后的文本文件hello.s编译后的汇编文件hello.o汇编后的可重定位目标文件hello链接后的可执行目标文件o_hello.txthello的反汇编文本文件

1.4 本章小结

       本章主要介绍hello的一生,尤其是p2p,020过程,并列出了本次实验所需的环境、工具以及过程中所生成的中间结果。

第2章 预处理

2.1 预处理的概念和作用 

预处理概念预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。由预处理器对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位来支持语言特性。

预处理作用处理头文件,指令如#include,将包含的文件复制到编译的源文件中;处理宏定义,用实际值替换用#define定义的字符串;处理条件编译指令,使得通过不同的宏定义来决定编译程序对哪些代码进行处理,将不必要的代码过滤掉;处理特殊符号;方便阅读、修改、移植和调试,利于模块化程序设计。

2.2 在Ubuntu下预处理的命令

命令cpp hello.c hello.i

图2.2.1 预处理命令

图2.2.2 预处理结果

2.3 Hello的预处理结果解析

图2.3.1 hello.i的部分截图

预处理后,hello.c转化为hello.i文件,对C语言源文件中进行了宏展开,头文件stdio.h、unistd.h、stdlib.h中的内容被包含进该文件中,宏定义、声明函数、结构体和变量的定义等内容。

2.4 本章小结

本章介绍了预处理的相关概念以及作用。之后对预处理的步骤进行实践,通过阅读hello.i可以验证预处理所做的行为,例如包含头文件内容等,并作了截图展示。预处理为后续的编译、汇编、链接等操作打下基础。

第3章 编译

3.1 编译的概念与作用

编译的概念编译器通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将.i文件翻译成.s文件,它包含一个汇编语言程序。它以高级程序设计语言书写的源程序作为输入,而输出汇编语言或机器语言表示的目标程序。

编译的作用检查语法,以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位;代码优化,生成更有效的目标代码;目标代码生成,将预处理后的程序编译为更接近计算机语言,更容易让计算机理解的语言。

3.2 在Ubuntu下编译的命令

命令g hello.i –S –o hello.s

图3.2.1 编译命令

图3.2.2 编译结果

3.3 Hello的编译结果解析

3.3.1 声明

.file声明源文件

.text代码节

.section:

.rodata只读代码段

.align数据或者指令的地址对齐方式

.string声明字符串(.LC0,.LC1)

.global声明全局变量(main)

.type声明类型为数据或函数

 图3.3.1 声明

3.3.2 数据

如图3.3.1所示,数据为两个字符串,作为printf函数的参数。

3.3.3 主函数main

根据hello.c得知,main函数中有局部变量i。如图3.3.2,局部变量i被放在栈-4(%rbp)处。main函数的两个参数argc和argv[],分别放在栈-20(%rbp)和 -32(%rbp)处。

 图3.3.2 主函数1

图3.3.3 主函数2

3.3.4 赋值和算数操作

通过mov指令来赋值,比如图3.3.3所示movq (%rax),%rax;movq %rax,%rsi等指令,以实现与函数中获得argv[1]等。

寄存器值的计算通过add和sub等指令实现,如addq $8, %rax等。图3.3.3所.L2部分addl $1, -4(%rbp)实现循环中i++语句。

3.3.5 数组

程序中数组为argv[],一个指针数组,作为main函数的参数和内部被调用的函数sleep(atoi(argv[3]))的参数。初始地址储存在-32(%rbp)中。

图3.3.4 指针数组

3.3.6 条件跳转控制转移

main函数中要求计算argc!=4,值为1则执行printf和退出,即如图3.3.5所示,比较argc=-20(%rbp)和立即数4的大小,如果相等则跳转到.L2处,为变量i进行声明;否则就进行函数操作,call puts和exit,即函数中的printf()和exit(1)。

 

图3.3.5

在声明变量i之后,无条件跳转到.L3处,如图3.3.6所示,比较i=-4(%rbp)和立即数7的大小,即函数中的循环for(i=0; i<8; i++),满足进入循环的条件则跳转到.L4。

 

图3.3.6

3.3.7 函数操作

机器指令如图3.3.2和3.3.3所示,C语言语句如图3.3.7所示

call puts@PLT调用puts函数,即printf("用法: Hello 学号 姓名 秒数!n");语句,输出字符串用法: Hello 学号 姓名 秒数!。

call printf@PLT调用printf()函数,即printf("Hello %s %sn",argv[1],argv[2]);语句,输出字符串。

 

图3.3.7 main函数部分

call exit@PLT调用exit()函数,即exit(1)语句,非正常退出。

call atoi@PLT调用atoi函数,即atoi(argv[3])语句,将字符串类型的数据转变成int类型的数据。

call sleep@PLT调用sleep函数,即sleep(atoi(argv[3]))语句,实现程序休眠,传入的参数atoi(argv[3])表示休眠秒数。

call getchar@PLT调用getchar()函数,读取缓冲区字符。

3.4 本章小结

本章论述了编译的概念和作用,并对hello.c程序的编译过程进行了截图展示。说明了hello.c的编译结果,描述了指令和语句之间的对应关系,展示了相应语句的汇编代码。编译器通过词法分析和语法分析,来检查原始代码有没有错误,在确认没有错误之后,编译器会按照一定的规范生成与原始代码等价的中间代码或汇编代码。通过阅读指令,明显可以看出,这个过程中,编译器可能会按照自己的理解,对原始代码结构和数据做出调整。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念把汇编语言翻译成机器语言的过程。在汇编语言中,用助记符代替操作码,用地址符号或标号代替地址码,把机器语言变成了汇编语言。用汇编语言编写的程序,机器不能直接识别。驱动程序运行汇编器as,将汇编语言的ASCII码文件翻译成机器语言的可重定位目标文件的过程称为汇编。

汇编的作用汇编过程将汇编代码转换为计算机能够理解并执行的二进制机器代码,这个二进制机器代码是程序在本机器上的机器语言的表示。

4.2 在Ubuntu下汇编的命令

命令g hello.s –c –o hello.o

 

图4.2.1 汇编命令

 

图4.2.2 汇编结果

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

4.3.1 ELF头

命令readelf -h hello.o

ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。此时,以小端码机器存储,文件类型为可重定位目标文件。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

图4.3.1 hello.o的ELF头内容

hello.o为64位文件;程序的入口地址为0x0,因为hell.o还未实现重定位;可重定位文件没有段头表;节头表的起始位置为1240;节头大小为64字节;节头数目为14。

4.3.2 节头表

命令readelf -S hello.o

节头表包含文件中出现的各个节的语义,包括节的类型、位置和大小等信息。 由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,可以观察到,代码是可执行的,不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。

节头表中一共有14项,其中第一项为空。剩下13项对应于可重定位文件中每一节的相关内容,其中包含每一节的名字,类型,地址,在文件中的偏移量,节的大小,访问权限,对齐方式等。例如.text为已编译程序的机器代码,.rodata为只读数据,.data为已初始化的全局和静态C变量,.bss为未初始化的全局和静态C变量,.symtab为符号表。

由于还未进行重定位,所以地址都未0。

图4.3.2 hello.o的节头表内容

4.3.3 符号表

命令readelf -s hello.o

.symtab符号表存放在程序中定义和引用的函数和全局变量的信息。每个可重定位目标文件在.symtab中都有一张符号表。和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。

图4.3.3 hello.o的符号表内容

由上图可以看出,符号表中存储了程序中定义和使用的各种符号,包括函数名,全局变量名等等。name代表符号名称,对于可重定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。

4.3.4 重定位节.rela.text

命令readelf -r hello.o

重定位节.rel.text是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改,而调用本地函数的指令则不需要修改。当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

 

图4.3.4 hello.o的重定位节内容

Offset是需要被修改的引用节的偏移。Info包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节,其中symbol标识被修改引用应该指向的符号,type指定重定位的类型。Type告知链接器应该如何修改新的应用。Attend是一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整。Name表示重定向到的目标的名称。

4.4 Hello.o的结果解析

objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

图4.4.1 反汇编代码

hello.s代码如图3.3.1~3.3.3所示。

代码表示反汇编代码中不仅有汇编代码,还有其对应的机器语言代码,即完全面向计算机的二进制数据表示的语言,包含操作码,数据,寄存器编号等内容,其中机器语言的每一个操作码,寄存器编号等都与汇编语言一一对应。机器语言中的数据采用小端序存储的二进制形式表示,而在汇编语言中采用的是顺序十六进制形式表示。反汇编代码mov、add等指令后无其他表示长度的字符如b等。

字符串操作hello.s中printf()函数中用.LC0来表示字符串地址,而反汇编代码中用0表示。原因是未重定位,无法确定其地址。

控制转移hello.s中函数被分为多个块,在运行时在多个块中进行跳转,即使用段名称进行跳转;而反汇编语言中没有进行分块,所有的代码是一整段,用相对地址进行跳转。目前未重定位,机器代码中跳转部分为零,它们将在链接之后被填写正确的位置。

函数调用hello.s中call后面为函数名称,而反汇编代码中call后面为相对地址。只有在链接之后才能确定运行执行的地址,目前目的地址是全0。

4.5 本章小结

本章介绍了汇编的过程。汇编器将汇编语言转化为机器语言,以供计算机识别并执行相关指令,得到了.o文件,即可重定位目标程序hello.o,可以通过readelf来查看其信息。重点讲述了可重定位文件的ELF头,节头表,符号表和重定位节.rela.text的内容。比较了反汇编结果与汇编文件hello.s的区别。

第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.2.1 链接命令

 

图5.2.2 链接结果

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

5.3.1 ELF头

命令readelf –h hello

 

图5.3.1 hello的ELF头内容

可执行目标文件的格式类似于可重定位目标文件的格式。ELF头描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。. text..rodata和.data节除了这些节已经被重定位到它们最终的运行时内存地址以外,与可重定位目标文件中的节是相似的。.init定义了一个小函数,叫做_init,程序的初始化代码会调用它。

hello的Magic是一个16字节的序列,ELF的大小为64bytes,节头大小64bytes,数据是小端序存储,Type说明文件是exec类型。hello的入口地址是0x4010f0,非零说明重定位工作已经完成。注意到hello的ELF头中program headers的偏移量非零,说明hello文件中比hello.o文件中多了一个段头表。

5.3.2 节头表

命令readelf –S hello

由于重定位工作已完成,hello节头表中每一节都有了实际地址。节头表对 hello中所有的节信息进行了声明,包括大小、在程序中的偏移量、程序被载入到虚拟地址的起始地址等。多出的节是为了能够实现动态链接,如.interp这一节包含动态链接器的路径名,动态链接器通过执行一系列重定位工作完成链接任务。

 

图5.3.2 hello的节头表内容

5.3.3 符号表

命令readelf –s hello

hello程序的符号表给出.dynsym的内容,共9例,包含编号Num、Value、Size、Type、Bind、Vis、Ndx、Name字段,Type指明类型是否为函数,Mame名称。之后给出.symtab符号表的内容,同样包含编号Num、Value、Size、Type、Bind、Vis、Ndx、Name字段,共51例。

 

图5.3.3 hello的符号表内容

5.3.4 段头表

命令readelf -l hello

ELF可执行文件被设计得很容易加载到内存,可执行文件的连续的被映射到连续的内存段。段头表描述了可执行目标文件的连续的片与连续的虚拟内存段之间的映射关系。从段头表中可以看到根据可执行目标文件的内容初始化为两个内存段,分别为只读内存段和读写代码段,即代码段和数据段。

图5.3.4 hello的段头表内容

offset是目标文件中的偏移;VirtAddr和PhysAddr指内存地址;FileSiz是目标文件中的段大小;MemSiz说明内存中的段大小;Flags是运行时访问权限;Align是对齐要求。

5.4 hello的虚拟地址空间

使用EDB加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

 

图5.4.1 程序开始

通过查看edb可以看出程序从0x400000开始。

从5.3.1的ELF头中可以看出程序的入口地址为0x4010f0,是第一条指令的地址,对应于5.3.2中的.text的地址。

 

图5.4.2 .text

.init节,即初始化代码需要调用的节的初始地址为0x401000,对应

 

图5.4.3 .init

由5.3.2中的.rodata的起始地址为0x402000,对应于

 

图5.4.4 .rodata

可以看出printf语句中的格式串%s %s位于.rodata节,属于只读数据。

.interp的起始地址为0x4002e0,偏移量为0x2e0,0x4002e0位置处放动态链接器的路径名。

 

图5.4.5 .interp

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

 

图5.5.1 命令

用命令objdump –d –r hello > o_hello.txt,将hello反汇编到o_hello.txt文件中。

通过比较hello反汇编和hello.o,可以得到一些不同,例如

在重定位结束以后,hello中的每个符号都有确定的地址,而hello.o的反汇编代码的地址从0开始,还未实现重定位的过程,每个符号还没有确定的地址。

hello含有在hello.o中没有的函数,这些都是在hello.c中没有定义却直接使用的函数,这些函数定义在共享库中,在链接时完成了符号解析和重定位,如printf、sleep等。

 

图5.5.2 其他函数

由于链接器完成了重定位过程,可以确定运行时的地址,hello中call、jmp指令后紧跟的是虚拟内存的确定地址,而在hello.o中紧跟着的是相对地址。

 

图5.5.3 地址跳转

当链接器完成符号解析,就把代码中的每个符号引用和一个符号定义关联起来。此时,链接器开始重定位步骤了,在这个步中,将合并输人模块,并为每个符号分配运行时地址。重定位由两步组成

重定位节和符号定义链接器将所有相同类型的节合并为同一类型新的聚合节。

重定位节中的符号引用链接器依赖于可重定位目标模块中称为重定位条目的数据结构修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。

ELF重定位条目的格式如下

    long offset;

    long type:32

         symbo1:32;

    long addend;

offset 是需要被修改的引用的节偏移;symbol标识被修改引用应该指向的符号;type告知链接器如何修改新的引用;addend 是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。

ELF定义了32种不同的重定位类型,这里只考虑两种最基本的重定位类型

R_ X86 64_ PC32重定位一个使用32位PC相对地址的引用。一个PC相对地址就是距程序计数器的当前运行时值的偏移量。当CPU执行一条使用PC相对寻址的指令时,它就将在指令中编码的32位值加上PC的当前运行时值,得到有效地址。

R_X86_64_3重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。

链接器重定位代码的伪代码如下

 

图5.5.4 链接器重定位代码的伪代码

在加载的时候,加载器会把重定位后的节中的字节直接复制到内存,不再进行任何修改地执行这些指令。

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

ld-2.31.so!_dl_start

0x7f48cf65a3b0

ld-2.31.so!_dl_init

0x7f48cf668b40

hello! _start

0x4010f0

libc-2.31.so! __libc_start_main

0x7f6fd3044fc0

hello!puts@plt

0x401030

hello!exit@plt

0x401070

hello!printf@plt

0x401040

hello!sleep@plt

0x401080

hello!getchar@plt

0x401050

libc-2.31.so!exit

0x7f6fd3067a70

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

动态链接把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序。动态链接把链接过程推迟到了程序运行时,在形成可执行文件时,还是需要用到动态链接库。

GOTGOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

GOT.PLTPLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

调用dl_init之前

调用dl_init之后

从图中可以看出,在dl_init调用之后,该处的两个8字节的数据都发生了改变。

5.8 本章小结

本章主要介绍链接链接的概念和作用,说明了链接生成可执行文件的过程。整个过程中用截图展示可执行文件的ELF信息,节的内容等。分析了程序是如何实现的动态链接的。

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念进程是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据、栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

进程的作用向用户提供了一种假象程序好像是系统中当前运行的唯一程序,独占使用处理器和内存;处理器无间断的执行程序中的指令,程序中的代码和数据是系统内存中唯一的对象。

6.2 简述壳Shell-bash的作用与处理流程

Linux系统中Shell是一个交互型应用级程序,代表用户控制操作系统中的任务,是命令行解释器,以用户态方式运行的终端进程。流程如下

在Shell命令行中键入$./hello,终端进程读取用户由键盘输入的命令行;分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量;检查首个命令行参数是否是一个内置的Shell命令如果不是内部命令,调用fork( )创建新进程/子进程;然后在子进程中,用获取的参数调用execve( )执行指定程序;如果用户没要求后台运行,即命令末尾没有&,否则Shell使用aitpid或ait等待作业终止后返回;如果用户要求后台运行,则Shell返回。

6.3 Hello的fork进程创建过程

在Shell命令行中键入$./hello命令,命令行会判断是否是一个内置的Shell命令,如果是内置命令则立即对其进行解释。否则将其看成一个可执行目标文件,再调用fork创建一个新的子进程并在其中执行。

终端程序通过调用fork函数创建一个子进程,子进程得到与父进程完全相同独立的一个副本,包括代码段、段、数据段、共享库以及用户栈,PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。fork函数只被调用一次,却会返回两次一次在父进程中,返回子进程的PID;一次在子进程中,返回0。

6.4 Hello的execve过程

execve函数加载并运行可执行目标文件hello,且带参数列表argv 和环境变量列表envp。只有当出现错误时,execve返回到调用程序;正常情况下,execve调用一次并从不返回。

参数列表argv 变量指向一个以null结尾的指针数组,其中每个指针都指向一个参数字符串。argv[0]是可执行目标文件的名。envp 变量指向一个以null结尾的指针数组,其中每个指针指向一个环境变量字符串,每个串都是形如“name=value”的“名字-值”对。

execve加载hello后,调用启动代码设置栈,并将控制传递给新程序的主函数main,当main开始执行时,用户栈的组织结构从高地址的栈底往低地址的栈顶依次为参数和环境字符串、以null 结尾的指针数组,其中每个指针都指向栈中的一个环境变量字符串、以null结尾的argv[ ]数组、系统启动函数libc_ start main的栈帧。

execve会删除已存在的用户区域,即删除当前进程虚拟地址的用户部分中已存在的区域结构;然后映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构;接着映射共享区域;设置程序计数器PC,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

逻辑控制流是PC值序列。进程会向每个程序提供一种独占使用处理器的假象,即使有其他程序在运行。如果使用调试器单步调试执行程序,我们会看到一系列的程序计数器的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。

并发流一个逻辑流在执行时间上与另一个逻辑流重叠。

内核模式和用户模式内核模式和用户模式不是两个进程,而是一个进程的不同模式,由一个模式位来控制,当设置了模式位时,进程就运行在内核模式中,这时候这个进程就可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,用户程序必须通过系统调用接口间接地访问内核代码和数据。运行程序代码的进程一开始是处于用户模式,只有当发生中断、故障或者陷入系统调用这样的异常时,转而去执行异常处理程序,这时进程才会变为内核模式。当它返回到应用程序代码时,处理器就把模式从内核模式改为用户模式。

上下文信息内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。

上下文切换内核为每个进程维持一个上下文,上下文就是在进程执行的某些时刻,内核可以决定枪战当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度。系统调用和中断也可能引发上下文切换。

进程hello初始运行在用户模式中,直到它通过执行系统调用sleep陷人到内核。内核中的陷阱处理程序请求来自磁盘控制器的DMA传输,并且安排在磁盘控制器完成从磁盘到内存的数据传输后,磁盘中断处理器。在上下文切换切换之前,内核代表进程hello在用户模式下执行指令,之后内核进行上下文切换将当前进程的控制权交给其他进程并执行,当定时器到时时发送一个中断信号,此时进入内核状态执行中断处理,重新运行hello进程。

当hello调用getchar之前,运行在用户模式,之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

6.6.1 hello的异常

hello在运行中,可能会出现以下异常

 

图6.6.1 异常

中断在hello程序执行的过程中来自外部I/O设备引起的异常。发生中断后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。当处理程序返回时,它就将控制返回给下一条指令。结果是程序继续执行,就好像没有发生过中断一样,

陷阱陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。陷阱处理程序将控制返回给下一条指令。

故障由错误情况引起,可能能被故障处理程序修复。比如缺页故障。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort 例程会终止引起故障的应用程序。

终止不可恢复的致命错误造成的结果,通常是硬件错误,比如DRAM或者SRAM位损坏的奇偶错误。

6.6.2 不停乱按包括回车

 

图6.6.2 乱按

如果乱按过程中没有按回车,则只会在屏幕上显示输入的内容。如果输入回车,则getchar读回车,并把回车前的字符串当作shell输入的命令。

6.6.3 Ctrl+Z

 

图6.6.3

如果输入Ctrl+Z会发送一个SIGTSTP信号给前台进程组的每个进程,结果是停止(挂起)前台作业。

6.6.4 Ctrl+C

 

图6.6.4

如果在程序运行过程中输入Ctrl+C,会让内核发送一个SIGINT信号给到前台进程组中的每个进程,结果是终止前台进程。

6.6.5 ps

 

图6.6.5

ps可以查看当前正在运行的部分进程。

6.6.6 jobs

 

图6.6.6

jobs可以查看任务列表。

6.6.7 pstree

 

图6.6.7

pstree可以查看进程树。

6.6.8 fg

 

图6.6.8

fg将一些停止的进程恢复到前台运行。它向处于停止状态的进程发送SIGCONT信号,恢复这些进程,并在前台开始运行。

6.6.9 kill

图6.6.9

kill向其他进程(包括自己),kill -9发送信号SIGKILL,杀死该pid的进程。

6.7本章小结

本章介绍了进程的概念和作用和Shell的处理过程。分析fork和execve函数的功能,展示在hello运行过程中,内核对其调度,异常处理程序为其将处理各种异常以及发送信号的情况。

第7章 hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

逻辑地址程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址,是由一个段标识符加上一个指定段内相对地址的偏移量,在hello中就是各部分在段内的偏移地址。

线性地址虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。在hello里面为虚拟内存地址。

虚拟地址CPU未开启分页功能时,线性地址就被当做最终的物理地址来用;若开启了分页功能,则线性地址就叫作虚拟地址。

物理地址用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应,即内存单元的绝对地址。在hello程序中就是虚拟内存地址经过翻译后获得的地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理

一个逻辑地址由两部分组成段标识符和段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器索引号。多个段描述符组成一个数组“段描述符表”,通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,由8个字节组成。索引号就是段描述符的索引。段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。

Base字段表示包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。段选择符中的T1字段若为0,则用GDT,否则用LDT。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

逻辑地址转换为线性地址的一般步骤

,给定一个完整的逻辑地址[段选择符段内偏移地址],

判断段选择符的T1为0或1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小,得到一个数组。

在这个数组中根据段选择符中前13位查找到对应的段描述符,获得基地址Base。线性地址为Base + offset。

7.3 Hello的线性地址到物理地址的变换-页式管理

计算机利用页表,通过MMU来完成从虚拟地址到物理地址的转换。页表将虚拟页映射到物理页,每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。页表是一个页表条目PTE数组,虚拟地址空间中的每个页在页表中的一个固定偏移量处都有一个PTE。PTE是由一个有效位和一个n个字段组成的,有效位表明该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置。

 

图7.3.1 地址翻译符号

线性地址VA即虚拟地址。VA被分为虚拟页号VPN与虚拟页偏移量VPO,CPU取出虚拟页号,通过页表基址寄存器来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号PPN,通过将物理页号与虚拟页偏移量VPO结合,得到由物理地址PPN和物理页偏移量PPO组合的物理地址。

MMU利用虚拟页号VPN来在虚拟页表中选择合适的PTE,当找到合适的PTE之后,PTE中的物理页号PPN和虚拟页偏移量VPO串联起来得到相应的物理地址。因为虚拟页大小和物理页大小相同,VPO与PPO相同。此时,物理地址就通过物理页号先找到对应的物理页,然后再根据物理页偏移找到具体的字节。

 

图7.3.2 使用页表的地址翻译

7.4 TLB与四级页表支持下的VA到PA的变换

7.4.1 利用TLB加速地址翻译

每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。MMU中包括的一个关于PTE的小的缓存,称为翻译后备缓存器TLB可以消除这样的开销。TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相联度。

 

图7.4.1 TLB命中

当TLB命中时的步骤CPU产生一个虚拟地址;MMU从TLB中取出相应的PTE;MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存或者主存;高速缓存/主存将所请求的数据字返回给CPU。

当TLB不命中时,MMU必须从L1缓存中取出相应的PTE。新取出的PTE存放在TLB中,可能会覆盖一个已经存在的条目。

7.4.2 多级页表

将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。VPN被分为i个部分,第一级VPN结合基址寄存器得到一个页表条目,其中存放下一级页表的基址,再结合VPN2,得到第三级页表基址,继续寻找,以此类推,直到确定对应的物理页号,与VPO结合,得到由PPN与PPO结合成的物理地址,用于物理地址寻址。

多级页表的使用从两个方面减少了内存要求。第一,如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在。第二,只有一级页表才需要总是在主存中。虚拟内存系统可以在需要时创建、页面调入或调出二级页表。

 

图7.4.2 使用多级页表的地址翻译

7.4.3 VA到PA的变换

处理器生成一个虚拟地址,并将其传送给MMU。MMU用VPN向TLB请求对应的PTE,如果命中,则跳过之后的几步。MMU生成PTE地址(PTEA),并从高速缓存/主存请求得到PTE。如果请求不成功,MMU向主存请求PTE,高速缓存/主存向MMU返回PTE。PTE的有效位为, MMU触发缺页异常,缺页处理程序确定物理内存中的牺牲页。若页面被修改,则换出到磁盘——写回策略。缺页处理程序调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来进程,执行导致缺页的指令。

CR3控制寄存器指向第一级页表(L1)的起始位置。CR3的值是每个进程上下文的一部分,每次上下文切换时,CR3的值都会被恢复。

 

图7.4.3

7.5 三级Cache支持下的物理内存访问

在从TLB或者页表中得到物理地址后,根据物理地址从Cache中寻找。到了L1里面以后,寻找物理地址要检测是否命中,不命中则紧接着寻找下一级Cache L2,接着L3,如果L3也不命中,则需要从内存中将对应的块取出放入Cache中,其中可能会发生块的替换等其它操作。这里使用到CPU的高速缓存机制,一级一级往下找,直到找到对应的内容。

7.6 hello进程fork时的内存映射

当fork函数被shell进程调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了shell进程的mm_ struct、 区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤

 

图7.7 hello进程execve时的内存映射

删除已存在的用户区域删除当前进程虚拟地址的用户部分中已存在的区域结构。

映射私有区域为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。

映射共享区域如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。

设置程序计数器PCexceve设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。

下一次调用这个进程时,它将从这个入口点开始执行。

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

缺页故障DRAM缓存不命中即为缺页。当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。

缺页中断处理若缺页P,缺页异常处理程序选择一个牺牲页,如果该牺牲页已经被修改了,那么内核就会将它复制回磁盘。内核修改牺牲页的页表条目。接下来,内核将P从磁盘复制到内存中,更新PTE,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

7.9.1 动态内存分配器

malloc函数返回一个指针,指向大小为至少参数字节的内存块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。在32位模式中,malloc返回的块的地址总是8的倍数。在64位模式中,该地址总是16的倍数。

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格,都要求显示的释放分配块

显式分配器要求应用显式地释放任何已分配的块。例如, C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。

隐式分配器也叫做垃圾收集器。分配器检测一个已分配块不再被程序所使用,那么就释放这个块。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

7.9.2 隐式空闲链表

隐式空闲链表的空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合,需要某种特殊标记的结束块。

 

图7.9.1 隐式空闲链表组织堆

隐式空闲链表的优点是简单,任何操作的开销,例如放置分配的块,要求对空闲链表进行搜索,该搜索所需时间与堆中已分配块和空闲块的总数呈线性关系,且系统对齐要求和分配器对块格式的选择会强制要求分配器上的最小块大小。没有块可以比这个最小值还小。例如,如果我们假设一个8字节的对齐要求,那么每个块的大小都必须是8字节的倍数。即使应用只请求一字节,分配器也仍然需要创建一个两字的块。

放置已分配的块当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大可以放置所请求的空闲块。分配器搜索方式的常见策略是适配、下一次适配和最佳适配。

当分配器找到一个匹配的空闲块时,通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。

当分配器找不到合适的空闲块,一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。

当分配器释放一个已分配块时,可能有其他空闲块与这个新释放的空闲块相邻。这些邻接的空闲块可能引起一种假碎片现象,有许多可用的空闲块被切割成为小的、无法使用的空闲块。而Knuth提出的一种采用边界标记的技术可以快速完成空闲块的合并

 

图7.9.2 使用边界标记的堆块格式

合并可以在常数时间内完成。

7.9.3 显示空闲链表

显示空闲链表是将空闲块组织为某种形式的显示数据结构。在每个空闲块中,都包含一个前驱和后继的指针。使用双向链表而不是隐式空闲链表,使适配的分配时间从块总数的线性时间减少到空闲块数量的线性时间。释放一个块的时间取决于空闲链表中块的排序策略

一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。使用LIFO的顺序和适配的放置策略,分配器会最先检查最近使用过的块。在这种情况下,释放一个块可以在常数时间内完成。如果使用了边界标记,那么合并也可以在常数时间内完成。

 

图7.9.3 使用双向空闲链表的堆块格式

另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序的适配比LIFO排序的适配有更高的内存利用率,接近最佳适配的利用率。

7.10本章小结

本章介绍了存储器地址空间、段式管理、页式管理,VA 到 PA 的变换、物理内存访问, hello进程fork时和execve时的内存映射、三级cashe的物理内存访问、缺页故障与缺页中断处理、包括隐式空闲链表和显式空闲链表的动态存储分配管理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化文件

设备管理unix io接口

一个Linux文件就是一个m字节的序列B0,B1,B2……Bm。所有的I/O设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种方式允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行打开文件;改变当前文件位置;读写文件;关闭文件。

8.2 简述Unix IO接口及其函数

8.2.1 Unix I/O接口

Unix I/O接口使所有的输入和输出都被当做对相应文件的读和写来执行。这使得所有的输入和输出都能以一种统一且一致的方式来执行

打开文件一个应用程序通过要求内核打开相应的文件来宣告它想要访问一个I/O设备。内核返回一个小的非负整数描述符,在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。

Linux shell创建的每个进程开始时都有三个打开的文件标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件< unistd.h>定义了常量STDIN_FILENO、STDOUT_ FILENO 和STDERR_ FILENO,用来代替显式的描述符值。

改变当前的文件位置对于每个打开的文件,内核保持着一个文件位置k,初始为0。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。

读写文件一个读操作就是从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会触发EOF)的条件。写操作就是从内存复制n个字节到一个文件,从当前文件位置k开始,然后更新k。

关闭文件应用完成对文件的访问后,通知内核关闭这个文件。内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。

8.2.2 Unix I/O函数

open()函数open函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件O_ RDONLY为只读;O _WRONLY为只写;O_ RDWR为可读可写,等等。

图8.2.1 open()函数

8.2.2 close()函数

close函数关闭一个打开的文件。

图8.2.2 close()函数

8.2.3 read()和rite()函数

read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。

rite函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

 

图8.2.3 read()和rite()函数

8.2.4 lseek()函数

lseek函数使应用程序显示地修改当前文件的位置。显式地为一个打开的文件设置偏移量,通常读写操作都是从当前文件偏移量处开始的,并使偏移量增加所读写的字节数。

 

图8.2.4 lseek()函数

8.3 printf的实现分析

printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。参数中采用可变参数的定义, fmt是一个char 类型的指针,指向字符串的起始位置。fmt也是个变量,它的位置,是在栈上分配的,它也有地址。

rite函数将buf中的i个元素写到终端。

 

图8.3.1 printf函数

printf用了两个外部函数,一个是vsprintf,还有一个是rite。

 

图8.3.2 vsprintf函数

vsprintf函数接受确定输出格式的格式字符串输入fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

rite函数将buf中的i个元素写到终端。

vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。vsprintf的输出到rite系统函数中。在Linux下,rite通过执行syscall指令实现了对系统服务的调用,从而使内核执行打印操作。内核会通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过显存vram对字符串进行输出。

[转]printf 函数实现的深入剖析 - Pianistx - 博客园

从vsprintf生成显示信息,到rite系统函数,到陷阱-系统调用int 0x80或syscall等。

字符显示驱动子程序从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

 

图8.4 getchar函数

getchar函数通过调用read函数来读取字符。当程序调用getchar时,程序等待用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾EOF则返回-1,且将用户输入的字符回显到屏幕。

异步异常-键盘中断的处理键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要介绍了Linux的I/O设备的基本概念和管理方法,以及Unix I/O接口及其函数,分析了printf和getchar函数的实现。

结论

用计算机系统的语言,逐条hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

从源文件hello.c诞生开始,hello经过hello.c预处理到hello.i文本文件、hello.i编译到hello.s汇编文件、hello.s汇编到二进制可重定位目标文件hello.o、hello.o链接最终生成可执行文件hello,此时hello正式产生。

接着键入运行命令,调用fork函数创建一个子进程,execve函数加载运行当前进程的上下文中加载并运行新程序hello。整个过程中, MMU、TLB、多级页表、三级cache进行地址翻译。

hello调用malloc函数从堆中申请内存,通过Linux I/O输入输出,执行printf函数,接受信号,例如回车,Ctrl+C,Ctrl+Z,ps等。

hello进程执行完成后,shell父进程对子进程进行回收,内核收回为其创建的所有信息。

计算机系统是由硬件和系统软件组成的,它们共同协作以运行应用程序。计算机内部的信息被表示为一组组的位,它们依据上下文有不同的解释方式。程序被其他程序翻译成不同的形式,开始时是ASCII文本,然后被编译器和链接器翻译成二进制可执行文件。处理器读取并解释存放在主存里的二进制指令。系统中的存储设备被划分成层次结构CPU 寄存器在顶部,接着是多层的硬件高速缓存存储器、DRAM主存和磁盘存储器。在层次模型中,位于更高层的存储设备比低层的存储设备要更快,单位比特造价也更高。层次结构中较高层次的存储设备可以作为较低层次设备的高速缓存。

操作系统内核是应用程序和硬件之间的媒介。它提供三个基本的抽象文件是对1/O设备的抽象;虛拟内存是对主存和磁盘的抽象;进程是处理器、主存和I/O设备的抽象。

hello.c是程序员遇到的最初始的程序,它的运行也包含了众多操作。计算机系统的设计与实现蕴含了多年以来众多技术人员的经验与智慧,通过不断地完善发展,才有了现在比较完备的体系。

附件

hello.c

源程序

hello.i

预处理后的文本文件

hello.s

编译后的汇编文件

hello.o

汇编后的可重定位目标文件

hello

链接后的可执行目标文件

o_hello.txt

hello的反汇编文本文件

参考文献

[1]  兰德尔E.布莱恩特,大卫R.奥哈拉伦. 深入理解计算机系统[M]. 北京机械工业出版社,2016.

[2]  printf函数实现的深入剖析[E]. [转]printf 函数实现的深入剖析 - Pianistx - 博客园. 2013.09.11.

[3]  Linux下可视化反汇编工具EDB基本操作知识[E].

Linux下 可视化 反汇编工具 EDB 基本操作知识_hahalidaxin的博客-CSDN博客_edb使用. 2018.11.24

[4]  百度百科. getchar. getchar(计算机语言函数)_百度百科.

Copyright © 2016-2025 www.caominkang.com 曹敏电脑维修网 版权所有 Power by