计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号 xxxxxxxxxx
班 级 xxxxxxx
学 生 xxx
指 导 教 师 xxx
计算机科学与技术学院
2019年12月
摘 要
本文主要是通过运用CSAPP课程中学习的各种知识,在Linux操作系统下分析研究hello程序的P2P和020过程,通过熟练使用各种工具,学习Linux框架下整个程序的声明周期,加深对课本知识的印象。
关键词:程序的生命周期;P2P;020;大作业;
(摘要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简介
P2P:hello.c通过预处理(cpp–hello.i)、编译(ccl-hello.s) 、汇编(as–hello.o)、重定位,链接过程生成了一个可执行文件hello
020:shell中运行,shell先fork一个子进程,在子进程中execve加载hello到虚拟内存空间,然后载入物理内存开始执行hello,将其输出,随后结束进程
1.2 环境与工具
1.2 环境与工具
硬件环境:
X64CPU; 8GHz; 8GRAM; 1TB HD
软件环境:
Windows10 64位;VMware14.12; Ubuntu 16.04 LTS 64位
使用工具:
Codeblocks,objdump ,edb
1.3 中间结果
hello.i 预处理后的文件
hello.s 编译之后的汇编文件
hello.o 汇编之后的可重定位目标文件
hello 链接后的可执行目标文件
hello.elf hello的elf文件
hello.txt hello.o的反汇编文件
helloasm.txt hello的反汇编文件
1.4 本章小结
介绍了P2P,020的过程,实验环境和工具,以及整个实验过程中的结果
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预编译程序读出源代码,对其中内嵌的指示字进行响应,产生源代码的修改版本,修改后的版本会被编译程序读入。具体为将包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些代码输出到一个 “.i” 文件中等待进一步处理。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
2.4 本章小结
本章内容:预处理的概念,作用和命令,对hello.c的预处理结果进行了解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译器(ccl)将文本文件hello.i翻译成文本文件hello.s的过程, hello.s包含一个汇编语言程序。
作用:把代码翻译成汇编语言。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1对于变量的处理:
1.int i:
i是一个局部变量。局部变量一般存在寄存器或堆栈中。变量数不足以占满6个寄存器,i应该被存放在寄存器中,同时由i的赋值语句i=0,可以看出i存放的位置是-4(%rbp)
2.字符串
程序中有两个字符串:“用法: Hello 学号 姓名 秒数!\n"和"Hello %s %s\n”
都存在只读数据段中。
3.数组:
程序中有一个数组argv[],argv[[1]]和argv[[2]]作为for循环中printf的参数。
由取argv[[1]]和argv[[2]]值的汇编语句可知argv的首地址是-32(%rbp)。
取argv[[2]]的汇编语句:
4.其他数据以立即数的形式出现。
3.3.2对于系统操作符与控制语句的处理:
1.赋值:
movl指令:
其他mov指令:
2.算数操作:
i++,i存在于-4(%rbp)中;
addl
其他相关整数运算操作:
3.关系操作
程序中出现的比较指令:
argc!=3被编译为:
for循环中的i<8被编译为:
4.数组/指针/结构操作
循环体中取数组元素值的操作:
一个指针数组,从数组相关的指令可以看出来,一个内容占8个字节,说明linux中一个地址的大小是8个字节.
3.4 本章小结
本章显示简述了编译的概念和作用,具体分析了一个c程序是如何被编译器编译成一个汇编程序的过程,还详细分析了不同的c语句和翻译成汇编语句之后的表示方法。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:将汇编语言翻译成机器指令,并将这些指令打包成一种叫做可重定位目标程序(hello.o )。
汇编的作用:将汇编指令转换成一条条机器可以直接读取分析的机器指令。
4.2 在Ubuntu下汇编的命令
gcc - c hello.s -o hello.o
4.3 可重定位目标elf格式
4.3.1ELF 头:用于总的描述ELF文件各个信息的段
4.3.2ELF节头:描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息
4.3.3重定位节:包含了.text节中需要进行重定位的信息。这些信息描述的位置,在由.o文件生成可执行文件的时候需要被修改(重定位)。在这个hello.o里面需要被重定位的有printf , puts , exit , sleepsecs , getchar , sleep ,rodata里面的两个元素(.L0和.L1字符串)
4.3.4.rela.eh_frame是eh_frame节的重定位信息。
hello.o的ELF格式,用readelf等列出其各节的基本信息,尤其是重定位项目分析。
4.4 Hello.o的结果解析
Hello.s:
Objdump -d -r 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.o与hello.s之间的差别。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程。
作用:把可重定位目标文件和命令行参数作为输入,产生一个完全链接的,可以加载运行的可执行目标文件。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
5.3.1ELF头:
5.3.2ELF节头:
.text节是保存了程序代码指令的代码节。一段可执行程序,存在Phdr,.text就会存在于text段中。由于.text节保存了程序代码,因此节的类型为SHT_PROGBITS。
.rodata 保存只读数据。类型SHT_PROGBITS。
.plt 过程链接表(Procedure Linkage Table),包含动态链接器调用从共享库导入的函数所必须的相关代码。存在于text段中,类型SHT_PROGBITS。
.bss节保存未初始化全局数据,是data的一部分。程序加载时数据被初始化成0,在程序执行期间可以赋值,未保存实际数据,类型SHT_NOBITS。
.got节保存全局偏移表。它和.plt节一起提供了对导入的共享库函数访问的入口。由动态链接器在运行时进行修改。如果攻击者获得堆或者.bss漏洞的一个指针大小写原语,就可以对该节任意修改。类型SHT_PROGBITS。
.dynsym节保存共享库导入的动态符号信息,该节在text段中,类型SHT_DYNSYM。
.dynstr保存动态符号字符串表,存放一系列字符串,代表了符号的名称,以空字符作为终止符。
.rel节保存重定位信息,类型SHT_REL。
.hash节,也称为.gnu.hash,保存一个查找符号散列表。
.symtab节,保存了ElfN_Sym类型的符号信息,类型SHT_SYMTAB。
strtab节,保存符号字符串表,表中内容被.symtab的ElfN_Sym结构中的st_name条目引用。类型SHT_SYMTAB。
.shstrtab节,保存节头字符串表,以空字符终止的字符串集合,保存了每个节节名,如.text,.data等。有个e_shsrndx的ELF文件头条目会指向.shstrtab节,e_shstrndx中保存了.shstrtab的偏移量。这节的类型是SHT_SYMTAB。
.ctors和.dtors节,前者构造器,后者析构器,指向构造函数和析构函数的函数指针,构造函数是在main函数执行前需要执行的代码,析构是main函数之后需要执行的代码。
5.3.3程序头表:
1.PTDR: 指定程序头表在文件及程序内存映像中的位置和大小。
2.INTERP: 指定要作为解释程序调用的以空字符结尾的路径名的位置和大小。对于动态可执行文件,必须设置此类型。
3.LOAD: 指定可装入段,通过p_filesz和p_memsz进行描述。文件中的字节会映射到内存段的起始位置。
4.DYNAMIC: 指定动态链接信息。
5.NOTE: 指定辅助信息的位置和大小。
6.GNU_STACK: 权限标志,标志栈是否是可执行的。
5.4 hello的虚拟地址空间
在edb中打开hello,通过Data Dump查看hello程序的虚拟地址空间各段信息。在Memory Regions选择View in Dump,在Data Dump中查看只读内存段和读写内存段的信息。
虚拟地址从0x00400000开始,展示了ELF可执行文件的连续的片被映射到连续的内存段的映射关系。目标文件中的偏移,内存地址,对齐要求,目标文件中段大小,内存中段大小,运行时访问权限等。
5.5 链接的重定位过程分析
反汇编命令:objdump -d -r hello
hello与hello.o主要有以下的不同:
1.增加新的函数:在hello.s中导入了诸如puts、printf、getchar、sleep等在hello程序中使用的函数,而这些函数的在hello_o.s中就没有出现
2.增加的节:hello_o.s从.text节开始,而hello.s从.init节开始。
3.函数调用:hello中无hello.o中的重定位条目,跳转和函数调用的地址在hello中都变成了虚拟内存地址。hello.o的反汇编代码,函数在链接之后确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
4.地址访问:在hello_o.s中调用函数都是使用call+<main+偏移量的做法>,而在hello_.s是直接使用call+<函数名> 的方法来直接调用的。
根据hello和hello.o的不同,分析链接过程:链接器ld将各个目标文件组装在一起,把.o文件中的各个函数段按照一定规则累积在一起,生成可执行文件。
5.6 hello的执行流程
载入:
_dl_start
_dl_init
开始执行
_start
_libc_start_main
_init
执行main:
_main
_printf
_exit
_sleep
_getcha
_dl_runtime_resolve_xsave
_dl_fixup
_dl_lookup_symbol_x
退出:
exit
5.7 Hello的动态链接分析
程序调用一个由共享库定义的函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。GNU编译系统使用延迟绑定的技术解决这个问题,将过程地址的延迟绑定推迟到第一次调用该过程时。
延迟绑定要用到全局偏移量表(GOT)和过程链接表(PLT)两个数据结构。如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,跳转到动态链接器中。每个条目都负责调用一个具体的函数。PLT[[1]]调用系统启动函数 (__libc_start_main)。从PLT[[2]]开始的条目调用用户代码调用的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[[1]]包含动态链接器在解析函数地址时会使用的信息。GOT[[2]]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。
原先0x00403ff0开始的global_offset表是全0的状态,在执行过_dl_init之后被赋上了相应的偏移量的值,表明dl_init操作是给程序赋上当前执行的内存地址偏移量 。
5.8 本章小结
本章介绍了链接的相关内容,分析了hello的elf文件,分析了hello 的虚拟地址空间、重定位过程、执行流程、动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中程序的实例。是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
作用:进程的概念为我们提供这样一种假象,就好像我们的程序是系统中当前运行的唯一程序一样,我们的程序好像是独占地使用处理器和内存,处理器好像是无间断地一条接一条地执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
shell的作用:为用户提供一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
1)从终端读入命令。
2)将输入字符串切分获得所有的参数
3)执行内置命令或者调用相应的程序为其分配子进程并运行
4)shell 接受键盘输入信号,并对信号进行相应处理
6.3 Hello的fork进程创建过程
在终端中输入./hello 学号 姓名 秒数,shell判断它不是内置命令,于是会加载并运行当前目录下的可执行文件hello.此时shell通过fork创建一个新的子进程(这个子进程得到与父进程用户级虚拟地址空间相同但是独立的一份副本),更改这个子进程的进程组编号。并准备在这个子进程执行execve。
6.4 Hello的execve过程
execve函数加载并运行可执行文件filename(hello),且带参数列表argv和环境变量envp.在execve加载了hello之后,它调用_start,_start设置栈,并将控制传递给新程序的主函数。
6.5 Hello的进程执行
(以下格式自行编排,编辑时删除)
其中hello用户模式和内核模式相互切换的过程叫做上下文切换
上下文切换:
1)保存当前的上下文
2)恢复某个先前被抢占的进程被保存的上下文
3)将控制传递给这个新恢复的进程
最后调用getchar时先是运行在前端读取缓冲区内容,再切换到内核调用相关函数后返回hello
Sleep调用
6.6 hello的异常与信号处理
异常的类别:
信号:
Ctrl-Z:如果输入Ctrl+Z会发送一个SIGTSTP信号给前台进程组的每个进程,,结果是停止前台作业,即我们的hello程序
Ctrl-C:如果在程序运行过程中输入Ctrl+C,会让内核发送一个SIGINT信号给到前台进程组中的每个进程,结果是终止前台进程,通过ps命令发现这时hello进程已经被回收。
Jobs:查看当前的关键命令(ctrl+Z/ctrl+C这类)内容,比如这时候就会返回ctrl+Z表示暂停命令
Pstree:用进程树的方法把各个进程用树状图的方式连接起来
Fg:他使后台挂起的进程继续运行
乱按:如果乱按过程中没有回车,这个时候只是把输入屏幕的字符串缓存起来,如果输入最后是回车,getchar把回车读入,并把回车前的字符串当作shell输入的命令.
6.7本章小结
本章为进程的定义和作用,shell的作用和处理流程,以及执行hello时的fork和execve过程。同时还分析了hello的进程执行和异常与信号处理过程。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。是hello.o中的相对偏移地址。
线性地址:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:程序访问存储器所使用的逻辑地址称为虚拟地址。是hello里的虚拟内存地址。
物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址。是hello里虚拟内存地址对应的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由两部分组成:段标识符、段内偏移量
段标识符是由一个16位长的字段组成,称为段选择符。其中前13位为索引号,后面三位包含一些硬件细节。
首先,给定一个完整的逻辑地址段选择符:段内偏移地址,
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。得到一个数组。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。
在保护模式下,分段机制就可以描述为:通过解析段寄存器中的段选择符 在段描述符表中根据 Index 选择目标描述符条目 Segment Descriptor,从目标描述 符中提取出目标段的基地址 Base address,最后加上偏移量 offset 共同构成线性地 址 Linear Address。
7.3 Hello的线性地址到物理地址的变换-页式管理
计算机利用页表,通过MMU来完成从虚拟地址到物理地址的转换。
PTBR是cpu里的一个控制寄存器,指向当前页表,n位的虚拟地址包括p位的虚拟页面偏移VPO和n-p位的虚拟页号VPN。MMU通过VPN来选择适当的PTE,将页表条目中的PPN(物理页号)和虚拟地址的VPO串联起来,就得到相应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
首先将VPN分成三段,对于TLBT和TLBI来说,优先在TLB中寻找对应的PPN,但可能出现缺页的情况,需要到页表中查找。此时,VPN被分成了更多段CR3是对应的L1PT的物理地址,进一步步递进往下寻址,越往下一层每个条目对应的区域越小,寻址越细致,在经过4层寻址之后找到相应的PPN与和VPO拼接起来。
7.5 三级Cache支持下的物理内存访问
L1 d-cache的结构如图所示:通过6-11位的组索引找到对应的组,将组中每一行的tag与CT比较,若标记位匹配且有效位为1,说明命中,根据0-5位的块偏移取出数据,如果没有匹配成功,则向下一级缓存中查找数据。取回数据后,如果有空闲块则放置在空闲块中,否则根据替换策略选择牺牲块。
7.6 hello进程fork时的内存映射
1.创建当前进程的内存描述符mm_struct、区域结构描述符vm_area_struct和页表的原样副本
2.两个进程的每个页面都标记为只读页面
3.两个进程的每个vm_area_struct都标记为私有
7.7 hello进程execve时的内存映射
当我们用./hello运行可执行文件hello时,先fork创建虚拟内存区域,当这个区域和父进程还是完全一样的,然后会调用execve(“hello”,NULL,NULL),加载并运行可执行文件hello,用hello程序有效替换了当前程序,加载并运行hello的步骤如下:
1)删除已存在的用户区域
2)映射私有区域(为hello程序的代码,数据,bss,栈区域创建新的区域结构),都是私有,写时复制的
3)映射共享区域,如我们的hello需要与libc.so动态链接,那么这些对象动态链接到这些程序,然后再映射到用户虚拟地址空间中的共享区域。
7.8 缺页故障与缺页中断处理
情况1:段错误:首先,先判断这个缺页的虚拟地址是否合法,那么遍历所有的合法区域结构,如果这个虚拟地址对所有的区域结构都无法匹配,那么就返回一个段错误(segment fault)
情况2:非法访问:接着查看这个地址的权限,判断一下进程是否有读写改这个地址的权限。
情况3:如果不是上面两种情况那就是正常缺页,那就选择一个页面牺牲然后换入新的页面并更新到页表。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
1.隐式空闲链表:
空闲块通过头部中的大小字段隐含地连接着。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。
(1)放置策略:首次适配、下一次适配、最佳适配。
首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配从上一次查询结束的地方开始。最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。
(2)合并策略:立即合并、推迟合并。
立即合并就是在每次一个块被释放时,就合并所有的相邻块;推迟合并就是等到某个稍晚的时候再合并空闲块。
2.显式空闲链表:
每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。使用双向链表使首次适配的时间减少到空闲块数量的线性时间。
空闲链表中块的排序策略:一种是用后进先出的顺序维护链表,将新释放的块放置在链表的开始处,另一种方法是按照地址顺序来维护链表,链表中每个块的地址都小于它后继的地址。
分离存储:维护多个空闲链表,每个链表中的块有大致相等的大小。将所有可能的块大小分成一些等价类,也叫做大小类。
分离存储的方法:简单分离存储和分离适配。
7.10本章小结
主要介绍了程序的存储结构,通过段式管理在逻辑地址到虚拟地址,页式管理从虚拟地址到物理地址。程序访问过程中的cache结构和页表结构,进程如何加载自己的虚拟内存空间,内存映射和动态内存分配。
这些所有的结构,都是为了两个目标,加快访问的速度,为每个进程分配一个独立的虚拟内存空间而不不会受到其他进程影响。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的 IO 设备都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
8.2 简述Unix IO接口及其函数
所有的输入输出以一种统一且一致的方式来执行:
1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2)Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
打开文件:
关闭文件:
读文件:
写文件:
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
Printf函数结构体:
其中调用了两个外部函数:一个是vsprintf,一个是write。
Vsprintf函数如下:
可以看出,这个函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。
Write函数如下:
接收了vsprintf返回的长度,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALL 代表系统调用syscall。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
先上代码:
getchar函数调用read函数,将整个缓冲区都读到buf里,并将缓冲区的长度赋值给n。返回时返回buf的第一个元素,除非n<0。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数。
结论
Hello在计算机中从出生到死亡经历的整个过程:
1 编写程序:写下hello程序的代码
2. 预处理:对带#的指令解析,生成hello.i文件
3. 编译:C语言程序编译成汇编语言程序,生成.s文件
4. 汇编:把汇编语言转换成机器代码,生成重定位信息,并生成.o文件。
5. 链接:与动态库链接,生成可执行文件hello
6. 创建进程:在shell利用./hello运行hello程序,通过fork函数为hello创建进程
7. 加载程序:通过加载器调用execve函数,加载现在进程的代码,数据等到进程自己的虚拟内存空间。
8. 执行指令:CPU取指令,顺序执行进程的逻辑控制流。虚拟地址通过MMU从页表里得到物理地址, 在通过这个物理地址去cache或者内存里寻找到信息
9. 异常(信号):程序执行过程中,如果从键盘输入各种命令,如Ctrl-Z等,会给进程发送一个信号,通过信号处理函数对信号进行处理。
10. 结束:程序执行结束后,父进程回收子进程,内核删除为这个进程创建的所有数据结构,释放空间。
做大作业的过程相当于复习了一下这个学期的知识,尤其是通过分析各种文件,深刻理解了编译汇编等过程。完成大作业过程中分析了hello程序运行各个阶段的底层实现,使我认识到计算机系统的运作过程。
附件
hello.c:源程序文件
hello.i:预处理后的文本文件
hello.s:编译后的汇编文件
hello.o:汇编后的可重定位文件
hello:链接后的可执行文件
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] https://www.cnblogs.com/pianist/p/3315801.html
[2] https://www.cnblogs.com/wangcp-2014/p/5146343.html
[3] https://blog.csdn.net/tuxedolinux/article/details/80317419
[4] https://www.cnblogs.com/zengkefu/p/5452792.html
[5] https://blog.csdn.net/gdj0001/article/details/80135196
[6]Linux GCC 常用命令
https://www.cnblogs.com/ggjucheng/archive/2011/12/14/2287738.html
[7] ELF中与动态链接相关的段
https://blog.csdn.net/virtual_func/article/details/48792087
[8] linux bash总结
http://www.cnblogs.com/skywang12345/archive/2013/05/30/3106570.html.
[9] x86在逻辑地址,线性地址,理解虚拟地址和物理地址
https://www.cnblogs.com/bhlsheji/p/4868964.html
[10] printf函数实现的深入剖析
https://www.cnblogs.com/pianist/p/3315801.html
[11] 键盘的中断处理
https://blog.csdn.net/xumingjie1658/article/details/6965176
[12]聊聊Linux动态链接中的PLT和GOT(3)——公共GOT表项
https://blog.csdn.net/linyt/article/details/51637832
[13] 深入理解计算机系统(第三版) Randal E.Bryant David R.O’Hallaron