Hello的一生
计算机系统
大作业
计算机科学与技术学院
2018年12月
本文主要是通过合理运用这个学期在计算机系统课程上学习的知识,分析研究hello程序在Linux下的P2P和020过程,通过熟练使用各种工具,学习Linux框架下整个程序的声明周期,加深对课本知识的印象。
关键词:程序的生命周期;进程;P2P;020;……;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式....................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程........................................................................ - 10 -
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 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
1.1 Hello简介
P2P过程:首先先有个hello.c的c程序文本,经过预处理->编译->汇编->链接四个步骤生成一个hello的二进制可执行文件,然后由shell新建一个进程给他执行。
020过程:shell执行他,为其映射出虚拟内存,然后在开始运行进程的时候分配并载入物理内存,开始执行hello的程序,将其output的东西显示到屏幕,然后hello进程结束,shell回收内存空间。
1.2 环境与工具
硬件环境:
X64CPU; 8GHz; 8GRAM; 1TB HD
软件环境:
Windows10 64位;VMware14.12; Ubuntu 16.04 LTS 64位
使用工具:
codeblocks,objdump,gdb,edb,hexedit
1.3 中间结果
hello.i(hello.c预处理之后的程序文本)
hello.s(hello.i编译成汇编语言之后的程序文本)
hello3.c(用于测试sleepsecs的值的程序文本)
hello3(hello3.c生成的可执行二进制文件)
hello.o(hello.s生成的二进制文件)
hello_o.s(hello.o反汇编的结果)
hello(可执行的hello二进制文件)
hello_.s(可执行文件hello,直接用objdump反编译之后的汇编代码)
hello.elf(可执行文件hello的elf表)
1.4 本章小结
磨刀不误砍柴工。
总的列了一下这整个实验中的中间结果和所在的软硬件环境还有使用的工具。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理又称预编译,(对于c/c++来说)预处理指的是在程序编译之前,根据以字符#开头的命令(即头文件/define/ifdef之类的),修改原始的c程序,例如大作业所提供的hello.c文件,就有三个头文件:stdio.h,unistd.h,stdlib.h(如下图)
预处理器所做的就是从系统头文件包中找到这三个头文件,并且把他们的内容插入hello.c的文本中,得到一个叫做hello.i的另类的c程序。
这样做的目的是方便编译器在对程序进行翻译的时候更加方便。
2.2在Ubuntu下预处理的命令
如果要将一个名为xxx.c的c语言程序预处理的话,我们用的语句是:
gcc -E -o xxx.i xxx.c
这样就会在当前目录下生成一个叫做xxx.i的预处理之后的程序。
让我们先来预处理一下hello.c
发现目录下面出现了一只hello.i
hello.c满打满算也就二十多行,那么这个hello.i有多大呢?
居然有三千多行……
2.3 Hello的预处理结果解析
我们可以发现,其实main函数里面的内容都没太大的变化,变化的是三个头文件,他们直接变成了那三个头文件的源码。虽然长度变长了许多,但是总的来说并不影响程序的可读性(毕竟现在仍然还是c语言的语法)。
而且预处理之后的程序中充斥着类似的:
用来描述使用的运行库在计算机中的位置。
还有这些:
用来声明可能使用到的函数的名字。
预处理操作把这些东西全都塞到了hello.i文本中。不过这样子也的确方便了编译器对他进行翻译成汇编语言的操作。
2.4 本章小结
本章节简单介绍了c语言在编译前的预处理过程,简单介绍了预处理过程的概念和作用,对预处理过程进行演示,并举例说明预处理的结果还有解析预处理的过程。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译编译,光听这两个字就知道他的概念了,他是一个将预处理好的高级语言程序文本翻译成能执行相同操作的汇编语言的过程。他的作用很简单,由于读懂高级语言对于计算机来说实在太过困难,所以需要一个编译器将高级语言直接转换成更接近机器语言的汇编语言,使得将高级语言转换成计算机可执行的二进制文件这个操作更加方便。
3.2 在Ubuntu下编译的命令
如果要编译一个名为xxx.i的预处理之后的程序的话,我们用的语句是:
gcc -S -o xxx.s xxx.i
这样就会在当前目录下生成一个叫做xxx.i的预处理之后的程序。
(上图便是生成的汇编程序)
3.3 Hello的编译结果解析
但是他只有六十多行,跟三千多行的hello.i比起来真的是小巫见大巫了。认真观察hello.s文本,我们可以发现他其实只翻译了hello.i中main函数及其周围的部分,头文件里面的内容他并没有翻译(难怪文本那么少)。实际上他其实也只需要翻译这个部分,要引用的头文件部分如何链接形成可执行文件在第4和第五章中会有解释。
3.3.1对于变量的处理
对于全局变量:源程序hello.c中有一个初始化过的全局变量sleepsecs,
这个变量是真的有毒,我们给他赋的初值是浮点数2.5,但是他是个整型变量,我们可以看一下sleepsecs的真实的值是多少。
我将hello.c复制了一份,加上了输出sleepsecs的操作。
编译运行之后的结果告诉我们,sleepsecs的值是2。但是观察汇编语句,并没有看见任何明显的类型转换的操作,所以我们认为这个类型转换是隐式的。
下图是编译过后形容sleepsecs的:
这个信息表示的是这么个内容:
这个变量在hello.c这个文件中声明;
他是个全局变量,他的变量名叫做sleepsecs;
由于他被存在.data节中。
他的类型是object(对象)
他占用的内存空间是4个字节
下面还有一个存放在.rodata节中的sleepsecs,他描述的是一个偏移量,表示sleepsecs在寄存器中的具体位置。
对于局部变量:hello.c中也有一个局部变量i:
很显然我们在hello.s的开头声明中找不到他,那么他在哪儿呢???
认真看看下面两张图:
原来这个变量i在用到他的时候才会出现啊……而且作为一个毫无人权的局部变量,他只会被存放在一个内存空间中(例子中是存放在-4(%rbp)这个位置)
小总结:
对于初始化过的全局变量,他的值会被存放在.data节中,在.rodata节中还会有一个专门指向他的偏移量;对于局部变量,只有在程序开始使用他(给他初始化)的时候才会申请一个内存空间给他。变量的类型转换是隐式转换。
3.3.2 对于关系操作符与控制语句的处理
在hello.c中出现了!=关系操作符(如下),
对于这个控制语句,编译器是转换成如下语句的:
我们从cmpl那儿开始分析,cmpl是一个比较语句,你可以将他与接下来的je放在一起看。假设je address前面有一个语句cmpl x,y ,则je判断的值是x-y的值,如果x-y的值等于0(即判断x是否等于y),则je条件达成,那么跳转到address位置,从address位置开始继续往下执行。否则的话则从je的下一条语句开始继续往下执行。
然后对于其他的操作符例如==,>,<,>=,<=,来说,编译的过程可以参照下面的表格:
3.3.3对于四则运算符及其复合语句的处理
加法:x=x+y
addq y,x
减法:x=x-y
subq y,x
乘法:x=x*y
imulq y,x
除法:z=x/y
movq x,z
cqto
idivq y
复合语句可以类比上图。
或者如下:
比如z=x+A*y+B(A,B为立即数)可以被表示成:
leaq B(x,y,A) , z
3.3.4对于数组/指针/结构的操作
数组:取数组的第i位一般是按照取数组头指针加上第i位的偏移量来操作的(如下图):
指针跟数组类似,如果x表示一个指针,rax表示其存储的寄存器,你要访问*x,那么就是(%rax)。
结构也是类似的,通过结构在结构体中的偏移量来访问。
3.3.5对于函数的操作
返回值:一个函数的返回值一般存在寄存器eax中,如果要设定返回值的话,那就先将返回值传入eax,然后再用ret语句返回。以hello.c为例子,具体操作如下:
(return 0操作)
函数调用及参数传递:如果你需要调用一个函数并且向其中传入参数的话,你需要先找几个寄存器,将参数传给这些寄存器,选择哪些寄存器要看你调用的函数的具体实现,然后再执行一个call跳转语句,跳转到你想要调用的函数的开头位置,此时程序就会从那个位置开始继续执行。下面是两个例子:
(exit(1)操作)
(printf(“Hello %s %s\n”,argv[1],argv[2])操作,其中的rdx,rsi,edi这些都是参数)
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
本章显示简述了编译的概念和作用,具体分析了一个c程序是如何被编译器编译成一个汇编程序的过程,还详细分析了不同的c语句和翻译成汇编语句之后的表示方法。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念是指的将汇编语言(xxx.s)翻译成机器指令,并将这些指令打包成一种叫做可重定位目标程序,并将这个结果保留在(xxx.o)中。这里的xxx.o是二进制文件。汇编过程的作用是将汇编指令转换成一条条机器可以直接读取分析的机器指令。
4.2 在Ubuntu下汇编的命令
如果要编译一个名为xxx.i的预处理之后的程序的话,我们用的语句是:
as xxx.s -o xxx.o
就会生成一个名为xxx.o的二进制文件。
4.3 可重定位目标elf格式
ELF Header
ELF Header::用于总的描述ELF文件各个信息的段。
Section Header:描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息
.rela.text:重定位节,这个节包含了.text(具体指令)节中需要进行重定位的信息。这些信息描述的位置,在由.o文件生成可执行文件的时候需要被修改(重定位)。在这个hello.o里面需要被重定位的有printf , puts , exit , sleepsecs , getchar , sleep ,rodata里面的两个元素(.L0和.L1字符串)
.rela.eh_frame是eh_frame节的重定位信息。
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.4 Hello.o的结果解析
hello.s的内容
将hello.o反汇编的内容
我们可以看出跟hello.s相比,hello.o反汇编之后虽然右边的汇编代码没有太大的差别(多了一些跳转位置的解释和注释),但是左边多了一大堆东西。在冒号前面的是运行时候的机器指令的位置,冒号后面的呢,就是每一行汇编语句所对应的机器指令啦。机器语言是完全由0/1构成的,在这里显示的时候表示成十六进制的
4.4.1分支跳转语句
我们可以发现,在hello.s中跳转到的目标位置都是用.L3/.L4来表示的,在hello.o反汇编之后,这些目标被用具体的地址位置代替。
4.4.2函数调用
在原先的hello.s中,调用一个函数只需被表示成call+函数名,但是在hello.o反汇编的结果中我们可以看见,这里的call是call一个具体的地址位置。
4.5 本章小结
本章简述了hello.s汇编指令被转换成hello.o机器指令的过程,通过readelf查看hello.o的ELF、反汇编的方式查看了hello.o反汇编的内容,比较其与hello.s之间的差别。学习了汇编指令映射到机器指令的具体方式。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将多个文件拼接合并成一个可执行文件的过程,链接行为可以在编译/汇编/加载/运行时执行。链接的存在降低了模块化编程的难度。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
这个链接命令啊……真的是非常的长……
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
这次的elf看起来没有hello.o的elf那么短小精悍,我就把他输到文件里面慢慢看吧……
照例先看看ELF Header
上次section headers节头表的数量还只有13个,现在有31个了……这下可够受的了。
Size表示的是节头表对应的节的大小
Address表示的是被载入到虚拟地址后的地址是多少
Offset表示的是这个节在程序里面的地址偏移量
5.4 hello的虚拟地址空间
我们用edb打开hello,可以在Data Dump窗口看见hello加载到虚拟地址中的状况:
可以看出程序是在0x00400000地址开始加载的,结束的地址大约是0x00400fff
再来分析分析elf里面的Program Headers:
PHDR:程序头表
INTERP:程序执行前需要调用的解释器
LOAD:程序目标代码和常量信息
DYNAMIC:动态链接器所使用的信息
NOTE::辅助信息
GNU_EH_FRAME:保存异常信息
GNU_STACK:使用系统栈所需要的权限信息
GNU_RELRO:保存在重定位之后只读信息的位置
其余的从.dynamic到.strtab节的内容是存放在0x00400fff后面
5.5 链接的重定位过程分析
继续来看这两张描述节头表的大图
分析一下比hello.o多出来的这些节头表:
.interp:保存ld.so的路径
.note.ABI-tag
.note.gnu.build-i:编译信息表
.gnu.hash:gnu的扩展符号hash表
.dynsym:动态符号表
.dynstr:动态符号表中的符号名称
.gnu.version:符号版本
.gnu.version_r:符号引用版本
.rela.dyn:动态重定位表
.rela.plt:.plt节的重定位条目
.init:程序初始化
.plt:动态链接表
.fini:程序终止时需要的执行的指令
.eh_frame:程序执行错误时的指令
.dynamic:存放被ld.so使用的动态链接信息
.got:存放程序中变量全局偏移量
.got.plt:存放程序中函数的全局偏移量
.data:初始化过的全局变量或者声明过的函数
接下来分析一下汇编程序。
将hello.o反汇编之后的结果存入hello_o.s
再来看看hello_o.s和hello_.s
(hello_o.s)
(hello.s)
我们可以发现,hello_o.s从.text节开始,而hello.s从.init节开始
(hello_.s)
在hello.s中导入了诸如puts、printf、getchar、sleep等在hello程序中使用的函数,而这些函数的在hello_o.s中就没有出现
(hello_o.s)
(hello_.s)
我们可以发现,在hello_o.s中调用函数都是使用call+<main+偏移量的做法>,而在hello_.s是直接使用call+<函数名> 的方法来直接调用的。
5.6 hello的执行流程
吐槽:edb太坑人了,我们用gdb吧……
程序名称
载入:
_dl_start
_dl_init
开始执行:
_start
_libc_start_main
_init
执行main:
_main
_printf
_exit
_sleep
_getchar
_dl_runtime_resolve_xsave
_dl_fixup
_dl_lookup_symbol_x
退出:
exit
5.7 Hello的动态链接分析
在edb调试之后我们发现原先0x00600a10开始的global_offset表是全0的状态,在执行过_dl_init之后被赋上了相应的偏移量的值。这说明dl_init操作是给程序赋上当前执行的内存地址偏移量,这是初始化hello程序的一步。
5.8 本章小结
本章介绍了链接的概念和作用,分析了hello的ELF格式,(edb让人头疼的使用方法),虚拟地址空间的分配,重定位和执行过程还有动态链接的过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是计算机程序需要进行对数据集合进行操作所运行的一次活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个应用程序,他在操作系统中提供了一个用户与系统内核进行交互的界面。他的处理过程一般是这样的:
- 读取用户的输入
- 分析输入内容,获得输入参数
- 如果是内核命令则直接执行,否则调用相应的程序执行命令
- 在程序运行期间,shell需要监视键盘的输入内容,并且做出相应的反应
6.3 Hello的fork进程创建过程
如图:
6.4 Hello的execve过程
每一个进程都有一段唯一属于自己的内存地址段,在execve运行时,开始先是从0x00400000(对于32位系统来说是0x8048000)开始程序的执行。先是从可执行文件中加载的内容,然后是运行时的堆栈和共享库的存储器映射区域。
6.5 Hello的进程执行
对于这个循环中的这两个语句,shell是这样子解决的:
调用getchar()时先是运行在前端hello进程中,然后调用时切换到内核进程的标准读取程序中,从键盘输入读取到一个字符之后再回到hello进程
在内核和前端之前切换的动作被称为上下文切换。
6.6 hello的异常与信号处理
ps命令是看当前尚未被终止的进程有哪些。
ctrl+Z操作:
这个操作向进程发送了一个sigtstp信号,让进程暂时挂起,输入ps命令符可以发现hello进程还没有被关闭。
ctrl+C操作:
这个操作向进程发送了一个sigint信号,让进程直接结束,输入ps命令可以发现当前hello进程已经被终止了。
fg命令:
他可以使后台挂起的进程继续运行。(见上图)先输出了5次,然后ctrl+Z挂起之后,fg命令又可以让他继续进行,然后把剩下的5次输出完。
jobs命令:
jobs命令可以查看当前的关键命令(ctrl+Z/ctrl+C这类)内容,比如这时候就会返回ctrl+Z表示暂停命令
pstree命令:
pstree是用进程树的方法把各个进程用树状图的方式连接起来(进程树太大这里截取一部分)
kill指令
这个指令向固定进程发送某些信号,比如kill -s 1 3077,就表示向PID为3077的进程hello,发送了一个sighup的信号,然后用fg命令让他继续运行就会出现上图所示的内容。
如果是信号30的话就是显示电源故障……不同的信号会返回不同的信息来描述这个信号。
6.7本章小结
本章介绍了进程的概念和作用,描述了shell如何在用户和系统内核之间建起一个交互的桥梁。讲述了shell的基本操作以及各种内核信号和命令,还总结了shell是如何fork新建子进程、execve如何执行进程、hello进程如何在内核和前端中反复跳跃运行的。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:又称相对地址,是程序运行由CPU产生的与段相关的偏移地址部分。他是描述一个程序运行段的地址。
物理地址:程序运行时加载到内存地址寄存器中的地址,内存单元的真正地址。他是在前端总线上传输的而且是唯一的。在hello程序中,他就表示了这个程序运行时的一条确切的指令在内存地址上的具体哪一块进行执行。
线性地址:这个和虚拟地址是同一个东西,是经过段机制转化之后用于描述程序分页信息的地址。他是对程序运行区块的一个抽象映射。以hello做例子的话,他就是一个描述:“我这个hello程序应该在内存的哪些块上运行。”
7.2 Intel逻辑地址到线性地址的变换-段式管理
先将逻辑地址分成段选择符+段描述符的判别符(TI)+地址偏移量的形式,然后先判断TI字段,看看这个段描述符究竟是局部段描述符(ldt)还是全局段描述符(gdt),然后再将其组合成段描述符+地址偏移量的形式,这样就转换成线性地址了。(下图有具体操作)
7.3 Hello的线性地址到物理地址的变换-页式管理
在这个转换中要用到翻译后备缓冲器(TLB),首先我们先将线性地址分为VPN(虚拟页号)+VPO(虚拟页偏移)的形式,然后再将VPN拆分成TLBT(TLB标记)+TLBI(TLB索引)然后去TLB缓存里找所对应的PPN(物理页号)如果发生缺页情况则直接查找对应的PPN,找到PPN之后,将其与VPO组合变为PPN+VPO就是生成的物理地址了。
7.4 TLB与四级页表支持下的VA到PA的变换
首先还是按照7.3所说的那样将VPN分成三段,对于TLBT和TLBI来说,如果可以在TLB中找到对应的PPN的话那肯定是最好不过的了,但是还有可能出现缺页的情况,这时候就需要到页表中去找。此时,VPN被分成了更多段(这里是4段)CR3是对应的L1PT的物理地址,然后一步步递进往下寻址,越往下一层每个条目对应的区域越小,寻址越细致,在经过4层寻址之后找到相应的PPN让你和和VPO拼接起来。(如图)
7.5 三级Cache支持下的物理内存访问
得到物理地址之后,先将物理地址拆分成CT(标记)+CI(索引)+CO(偏移量),然后在一级cache内部找,如果未能寻找到标记位为有效的字节(miss)的话就去二级和三级cache中寻找对应的字节,找到之后返回结果。(如图)
7.6 hello进程fork时的内存映射
先来介绍两个概念:
mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间
vm_area_struct(区域结构描述符):描述了进程的虚拟内存空间的一个区间
在用fork创建虚拟内存的时候,要经历以下步骤:
- 创建当前进程的mm_struct,vm_area_struct和页表的原样副本
- 两个进程的每个页面都标记为只读页面
- 两个进程的每个vm_area_struct都标记为私有,这样就只能在写入时复制。
7.7 hello进程execve时的内存映射
- 删除已存在的用户区域
- 创建新的私有区域(.malloc,.data,.bss,.text)
- 创建新的共享区域(libc.so.data,libc.so.text)
- 设置PC,指向代码的入口点
(下图为创建的进程在地址段中表示)
7.8 缺页故障与缺页中断处理
情况1:段错误:首先,先判断这个缺页的虚拟地址是否合法,那么遍历所有的合法区域结构,如果这个虚拟地址对所有的区域结构都无法匹配,那么就返回一个段错误(segment fault)
情况2:非法访问:接着查看这个地址的权限,判断一下进程是否有读写改这个地址的权限。
情况3:如果不是上面两种情况那就是正常缺页,那就选择一个页面牺牲然后换入新的页面并更新到页表。
7.9动态存储分配管理
在程序运行时程序员使用如上图描述的动态内存分配器给引用程序分配内存,动态内存分配器的维护着一个进程的虚拟内存(堆)。分配出来的动态的内存在地址段中按照下图的位置给堆栈分配内存空间。这个称为堆。
由于编写hello程序的是C语言,而C语言的内存分配器为显式分配器,所以这里讨论显示分配器的内存管理方式。
malloc(size_t size)每次声明内存空间都要保证至少分配size_t大小的内存,保证双字对齐,每次必须从空闲块中分配空间,在申请空间的时候要记得将空闲的空间碎片合并,这样可以尽量减少浪费。
显式空闲链表对空闲块的分配方法见下图:
7.10本章小结
本章介绍了储存器的地址空间,讲述了虚拟地址、物理地址、线性地址、逻辑地址的概念,还有进程fork和execve时的内存映射的内容。描述了系统如何应对那些缺页异常,最后描述了malloc的内存分配管理机制(C语言为例),想想为了运行一个程序还需要分配各种内存地址空间,还要回收碎片,真的是很麻烦呢QAQ,但是作为严谨的操作系统,这种麻烦的事情带来的却是系统的稳定运行。(当一个操作系统是真的辛苦)
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
文件的类型:
- 普通文件(regular file):包含任意数据的文件。
- 目录(directory):包含一组链接的文件,每个链接都将一个文件名映射到一个文件(他还有另一个名字叫做“文件夹”)。
- 套接字(socket):用来与另一个进程进行跨网络通信的文件
- 命名通道
- 符号链接
- 字符和块设备
设备管理:unix io接口
- 打开和关闭文件
- 读取和写入文件
- 改变当前文件的位置
8.2 简述Unix IO接口及其函数
打开和关闭文件:
1.open()函数:这个函数会打开一个已经存在的文件或者创建一个新的文件
如果open的返回值为-1则说明其打开该文件失败
2.close()函数:这个函数会关闭一个打开的文件
读取和写入文件:
1.read()函数:这个函数会从当前文件位置复制字节到内存位置
2.write()函数:这个函数从内存复制字节到当前文件位置
读写文件时,如果返回值<0则说明出现错误
改变文件位置:
lseek()函数
8.3 printf的实现分析
printf需要做的事情是:接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。
上面是printf的代码,我们可以发现,他调用了两个外部函数,一个是vsprintf,还有一个是write。
从上面vsprintf函数可以看出,这个函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。
write函数是将buf中的i个元素写到终端的函数。
Printf的运行过程:
从vsprintf生成显示信息,显示信息传送到write系统函数,write函数再陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序。从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
可以看出,这里面的getchar调用了一个read函数,这个read函数是将整个缓冲区都读到了buf里面,然后将返回值是缓冲区的长度。我们可以发现,如果buf长度为0,getchar才会调用read函数,否则是直接将保存的buf中的最前面的元素返回。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章节讲述了一下linux的I/O设备管理机制,了解了开、关、读、写、转移文件的接口及相关函数,简单分析了printf和getchar函数的实现方法以及操作过程。
(第8章1分)
结论
终于到了结束的时候,我们来回顾以下可怜的hello的一生,看看他是怎样被我一个懵懂的大学生一步步从一个普通的C语言程序变成一个完整的可以执行的文件,然后运行之后又默默无闻地消失在我们的trash里面(划掉)说笑了,其实是消失在进程的海洋里。可怜的hello先是经过预处理和编译,然后汇编成二进制的hello.o,再用链接器往他身上链上一堆ctri,ctr1,crto等东西,让他成为了一个真正可以被linux系统执行的二进制文件,但是此时他的一生才走到了一半!我们运行了他,暂停了他,然后脸滚键盘(×),用字符塞满缓冲区、用kill瞎发送信号,让他花式被终止。最后一下ctrl+C,他的进程可算是寿终正寝了,但是我们还在他的进程过程中分析他的内存空间和I/O机制,不得不说,为大作业献身的hello程序值得我们的尊敬。
在做大作业的过程中,感觉就像是复习一样回顾了一整个学期的计算机系统的课程,回顾了很多调试器的使用方法,比如edb(坑人)之类的,感觉对整个程序的运行过程有了一个新的认识,对这样一套操作更加熟悉了。
(结论0分,缺少 -1分,根据内容酌情加分)
附件
(附件0分,缺失 -1分)
hello.i(hello.c预处理之后的程序文本)
hello.s(hello.i编译成汇编语言之后的程序文本)
hello3.c(用于测试sleepsecs的值的程序文本)
hello3(hello3.c生成的可执行二进制文件)
hello.o(hello.s生成的二进制文件)
hello_o.s(hello.o反汇编的结果)
hello(可执行的hello二进制文件)
hello_.s(可执行文件hello,直接用objdump反编译之后的汇编代码)
hello.elf(可执行文件hello的elf表)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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] https://www.cnblogs.com/pianist/p/3315801.html
[8] https://www.cnblogs.com/wangcp-2014/p/5146343.html
[9] https://blog.csdn.net/tuxedolinux/article/details/80317419
[10] https://www.cnblogs.com/zengkefu/p/5452792.html
[11] https://blog.csdn.net/gdj0001/article/details/80135196
(参考文献0分,缺失 -1分)