计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 120L021515
班 级 2003002
学 生 张靖宇
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
摘 要
本文通过研究hello.c在linux系统下的整个生命周期。通过对hello.c的概述;hello.c的预处理;编译;汇编;链接;一直到hello.c的进程;存储;IO管理。了解hello的一生。旨在通过对hello程序的运行过程分析,对程序运行过程有更进一步的了解
目 录
第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: From Program to Process,要生成可执行文件,首先要进行预处理,其次要进行编译生成汇编代码,接着进行汇编处理生成目标文件,目标文件通过链接器形成一个可执行文件。运行进程时,操作系统为其分配虚拟地址空间,hello被逐渐地载入物理内存。操作系统提供异常控制流等强大的工具,不断对系统中运行着的进程进行调度。在运行的过程中,程序通过Intel内存管理机制一步步访问逻辑地址、虚拟地址、物理地址,从而进行数据交换,还可以通过IO机制进行输入输出交互。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
软件:
Ubuntu 20.04 LTS虚拟机
gdb
edb
Visual Studio
Hexedit
硬件:
Intel Core i5 10210U
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
1.4 本章小结
这一章主要是环境配置,为之后的实验做准备
第2章 预处理
2.1 预处理的概念与作用
1.预处理概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
2.预处理作用:根据源代码中的预处理指令修改源代码,预处理从系统的头文件包中将头文件的源码插入到目标文件中,宏和常量标识符已全部被相应的代码和值替换,最终生成.i文件。
2.2在Ubuntu下预处理的命令
cpp hello.c > hello.i
图1 Ubuntu对hello.c进行预处理
2.3 Hello的预处理结果解析
最开始一段,是对hello.c中的宏进行宏展开,
图2 hello.i开头内容截图
程序的源代码在末尾:
图3 hello.i结尾内容截图
2.4 本章小结
本章介绍了预处理的概念和作用,并且在Ubuntu下使用linux命令对hello.c进行了预处理,还对hello.i中的内容进行了分析,加深了认识。
第3章 编译
3.1 编译的概念与作用
编译:利用编译程序从源语言编写的源程序产生目标程序的过程。
作用:把源程序翻译成目标程序,翻译成等价的计算机汇编语言。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
图3 生成hello.s
3.3 Hello的编译结果解析
3.3.1 数据
3.3.1.1 字符串
共有两个字符串,都在只读数据段中
图4 hello.s开头内容
其中,.file是声明源文件,.text是代码节,.section是各部分开始,.rodata是只读数据段,.string标识这是一个字符串。
可以看到,在.rodata段里有两个字符串存储。
3.3.1.2 argc与argv
用户传给main的参数argc和argv也被放到了堆栈里,如图:
图5 hello.s中argc与argv对应汇编
我们可以看到,argc被放到了-20(%rbp)的位置,而argv是被放到了-32(%rbp)的位置。
3.3.1.3 局部变量i
在代码段的.L2段,局部变量i被分配了空间,并且初始化为0
图6 hello.s中局部变量i对应汇编
3.3.1.4 立即数
在汇编代码中以$<立即数>的形式表示
图7 hello.s中立即数示例
3.3.2 操作
3.3.2.1 赋值操作
for循环中,局部变量i被赋值为0,具体实现如下所示:
图8 局部变量i赋值汇编
使用mov指令,把数据存入内存或寄存器
3.3.2.2 类型转换
在hello.c中,使用atoi把字符显式转化为整型。
图9 atoi转化
在汇编代码中,直接使用call指令,执行已经定义过的函数
图10 call sleep函数
各种类型可以按照规则进行转换
3.3.2.3 算数操作
在for循环中,i使用了自增操作符++,在汇编代码中体现如下:
图11 addl指令增加变量值
也就是利用add指令,对i加1
3.3.2.4 关系操作
在hello.c中,使用了关系判断(!=判断不等):
图12 条件判断语句
在汇编中,用条件跳转je来进行实现
图13 条件判断语句对应汇编
只有在相等的时候,才会进入下面的代码,否则会直接结束。
还有对循环中i<8的判断,使用条件跳转来实现:
图14 i<8循环对应汇编
3.3.2.5 数组/指针/结构操作
在hello.c中,对argv数组进行了数组取值:
图15 argv数组取值
在汇编代码中:
把argv[1]和argv[2]传给printf(%rdx和%rsi是第一、二个参数,%rax先变成argv[1]、[2]的地址,然后从内存中取值并复制给参数寄存器)
图16 argv传值汇编
3.3.2.6 控制转移
if语句直接使用条件跳转实现
图17 条件语句汇编
for语句使用比较指令和跳转指令来实现,在循环体内循环变量自增:
图18 for循环汇编实现
3.3.2.7 函数操作
main函数:
参数传递:argc,argv[]
函数调用:系统启动函数调用
函数返回:设置%eax为0,相当于return 0
图19 返回值设置
sleep、printf等函数:
参数传递:
printf: argv[1],argv[2]
exit:1
sleep: atoi(argv[3])
getchar:无参数
函数调用:汇编代码中使用call指令调用
3.4 本章小结
本章介绍了编译的概念以及作用,在Ubuntu下使用编译命令编译出了汇编代码文件,同时通过对汇编代码与C代码对应解析,对程序的各个段和汇编指令有了更深一步的了解。
第4章 汇编
4.1 汇编的概念与作用
概念:把汇编语言翻译成机器语言的过程
作用:将汇编语言翻译成机器语言,从而让机器执行
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o
图20 hello.o生成
4.3 可重定位目标elf格式
4.3.1 elf文件典型结构:
图21 elf文件典型结构
4.3.2生成elf文件:
图22 hello.o elf文件生成
4.3.3 elf文件结构分析:
4.3.3.1 elf头:
包含.o文件各个节的类型、位置、所占空间大小
图23 elf文件内容
4.3.3.2 节头
记录各个节的名称、类型、地址等信息
图24 elf文件内容
4.3.3.3 重定位节
各个段引用的外部符号等在链接的时候需要对这些位置的地址进行重定位,从而得到正确的引用地址。
hello.c中,需要被重定位的有:.rodata中的printf格式串,exit,printf,sleep,getchar函数等符号。
图25 elf文件内容
4.3.3.4 符号表
.symtab是符号表,存放在程序中定义的和引用的函数和全局变量的信息。
图26 elf文件内容
4.4 Hello.o的结果解析
4.4.1 生成反汇编文件:
图27 hello.o反汇编文件生成
查看生成文件:
图28 反汇编文件内容
4.4.2 机器语言与汇编语言的映射关系
对于一段机器语言,最开始的一个字节对应着操作码,映射到汇编语言的一个指令;接下来是这个指令规定的格式,按照这个格式,接下来的特定长度数据可能会被解释为操作数、地址,等到该指令翻译结束,读取下一个字节作为操作码,以此类推,最后把机器语言可以唯一地映射到汇编语言。
4.4.3 与hello.s对照分析
数的表示:hello.s中的操作数是十进制, 而hello.o反汇编中的操作数是16进制
hello.s:
图29 hello.s操作数示例
hello.o反汇编:
图30 反汇编操作数示例
控制转移:
hello.s使用段名称进行跳转,而hello.o反汇编已经使用虚拟地址进行跳转,目前还是等待重定位状态,是虚拟地址;在链接过程中将会填写正确的位置
hello.s:
图31 hello.s跳转地址示例
hello.o反汇编:
图32 反汇编跳转地址示例
函数调用:
hello.s直接使用函数名称作为call指令的参数,而反汇编代码中call使用的是虚拟地址,跟上面的控制转移类似。
hello.s:
图33 hello.s函数调用示例
hello.o反汇编:
图34 反汇编函数调用示例
4.5 本章小结
本章介绍了汇编的概念和作用,在Ubuntu下生成了汇编文件和elf文件,并且分析了它的内容和结构,同时,对生成的hello.o文件进行反汇编,与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
图35 链接生成可执行文件
5.3 可执行目标文件hello的格式
生成elf:
图36 elf文件生成
elf头:
图37 elf文件内容
节头:(包含各段起始地址、大小等信息)
图38、39 elf文件内容
重定位节:
图40 elf文件内容
符号表:
图41 elf文件内容
图42 elf文件内容
5.4 hello的虚拟地址空间
由上面的elf文件可以知道,虚拟地址是从0x400000开始的。所以我们通过edb进行调试,并且在进入main函数之后查看0x400000的内存情况:
图43 edb调试内存截图
根据这里的内存情况,我们就可以对照5.3的elf表来查看各个段的数据,比如.rodata段,开始地址是0x402000,size是0x3b,就可以对应找到所有数据。
图44 .rodata段信息
5.5 链接的重定位过程分析
5.5.1 hello与hello.o的不同
5.5.1.1 新增节
在hello中,新增了.init、.plt和.plt.sec三个节
图45、46、47 .init、.plt和.plt.sec三个节信息
5.5.1.2 新增函数
新增了在hello.c中用到的库函数,比如printf,getchar等等
图48 库函数
5.5.1.3 绝对地址
hello中,无论是函数调用还是控制流跳转都由相对地址变成了绝对地址,在重定位过程中完成了这一转换
hello:
hello.o:
图49、50 hello与hello.o跳转地址示例
5.5.2 链接过程
链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照依赖的顺序累积在一起,最后为所有引用找到依赖。
5.5.3 重定位过程
当汇编器遇到对最终位置未知的目标引用,就会生成一个重定位条目,告诉链接器如何修改这个引用。
编译器和汇编器通过合并各链接文件的节,生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向唯一的运行时内存位置。
5.6 hello的执行流程
从开始到结束:
_start 0x4010f0
_libc_start_main 0x00007f6bce6dffc0
图51、52 edb调试运行过程截图
按照相同的操作,获取调用函数,如下:
main 0x401125
printf 0x401040
exit 0x401070
sleep 0x401080
getchar 0x401050
5.7 Hello的动态链接分析
在调用dl_init之后,.got.plt段的数据会变化,查看.got.plt段信息
图53 .got.plt段信息
得知段开始地址是0x404000,在调用dl_init前后查看段数据:
调用之前:
调用之后:
图54、55 调用dl_init前后.got.plt段数据变化
段数据发生变化。
5.8 本章小结
本章主要介绍了链接的概念和作用,在Ubuntu里通过命令链接生成了可执行文件,并且对比了可执行文件与.o文件的elf文件结构,加深了对于链接、重定位等过程的认识。
第6章 hello进程管理
6.1 进程的概念与作用
概念:一个执行中程序的实例
作用:由于程序是静态的,我们看到的程序是存储在存储介质上的,它无法反映出程序执行过程中的动态特性,而且程序在执行过程中是不断申请资源,程序作为共享资源的基本单位是不合适的。所以我们使用进程作为共享资源的基本单位。
6.2 简述壳Shell-bash的作用与处理流程
作用:
作用:为使用者提供操作界面,接收用户命令,然后调用相应的应用程序。本质是一个命令行解释器。
bash 是一个为GNU项目编写的Unix shell,也就是linux用的shell。
处理流程:
(1)读入命令
(2)切分命令行,存入字符串数组
(3)检查是否为内置命令
(4)如果不是,调用fork函数创建子进程,再使用execve加载对应程序执行
(5)在执行途中,接收从键盘、内核和其他程序发出的信号并做出对应处理,直到停止
6.3 Hello的fork进程创建过程
shell中输入./hello 120L021515 张靖宇 5之后,首先shell对命令行进行切分,存入字符串数组,再检查hello是否为内置命令,发现不是,于是调用fork函数创建子进程,子进程大部分与父进程相同,但使用execve函数加载了./hello执行程序,拥有自己的PID。
6.4 Hello的execve过程
(1)删除已存在的用户区域和之前在用户部分中已存在的结构。
(2)映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss请求二进制零,映射匿名文件,栈和堆也初始化为零。
(3)映射共享区:例如Hello程序与标准C库libc.so链接,这些对象动态链接到Hello,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器:exceve设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
6.5.1上下文信息
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
6.5.2进程时间片
进程的时间片是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间(在抢占内核中是:丛进程开始运行直到被抢占的时间)。时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。时间耗完时,控制流通过上下文切换从一个进程传递到另一个进程,在进行进程上下文切换时,操作系统把换下进程的寄存器上下文保存到系统级上下文中的现场信息位置。
6.5.3 用户态和核心态切换
为了能让处理器安全运行,需要限制应用程序可执行指令所能访问的地址范围。因此划分了用户态与核心态。
核心态可以说是拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
6.6.1 异常的信号以及异常种类
图56 异常的信号以及异常种类图示
6.6.2 运行结果
6.6.2.1 正常运行
图57 正常运行
6.6.2.2 按回车
图58 回车运行
回车对于程序运行并没有什么影响,不过回车符存入了缓冲区,被之后的getchar函数进行读取,同时,执行完之后还在前台产生了几个空行。
6.6.2.3 按ctrl+z
运行过程中按ctrl+z,发送SIGSTP信号,前台进程被挂起
图59 ctrl+z运行
使用jobs命令查看当前运行的jobs,看到hello进程没有终止:
图60 jobs命令结果
[1] 由于是在zsh中,使用fg %1将job调回前台(这跟bash中命令有所不同)
图61 fg命令结果
6.6.2.4 按ctrl+c
进程收到SIGINT信号,直接结束,在jobs命令结果中无显示
图62 ctrl+c运行
jobs命令:
图63 jobs命令结果
6.6.2.5 中途乱按
与按回车一样,不会对当前程序运行产生影响,会在shell前台执行结束之后当做命令行输入
图64 中途乱按运行结果
6.6.2.6 kill命令
首先把前台进程挂起,在ps和jobs命令结果中都能看到hello进程
图65 kill运行结果
使用kill命令向hello进程发送SIGKILL信号,hello进程结束
图66 kill后jobs和ps运行结果
6.7本章小结
本章介绍了进程的概念以及作用,介绍了shell执行hello程序的整个过程,同时通过ps,jobs等命令加深了对进程运行状态以及信号处理机制的理解。
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1 逻辑地址
逻辑地址是指由程序产生的与段相关的偏移地址部分。在这里指的是hello.o中的地址内容。
7.1.2 线性地址
线性地址是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
7.1.3 虚拟地址
又称相对地址,是程序运行由 CPU 产生的与段相关的偏移地址部分。他是描述一个程序运行段的地址。有时我们也把逻辑地址称为虚拟地址。
7.1.4.物理地址
CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符:段内偏移量。段标识符是由一个 16
位长的字段组成,称为段选择符。其中前 13 位是一个索引号。后面 3 位包含一些硬件细节。
给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的值,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。基地址+ 偏移量 = 线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
将各进程的虚拟空间划分成若干个长度相等的页,页式管理把内存空间按页的大小划分成片或者页面,然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
7.4 TLB与四级页表支持下的VA到PA的变换
在Hello程序中,52位的虚拟地址,被划分成了4级分页,每一级9位,每一个片用作下一级页表的偏移量。最后获取到L4页表得到40位的PPN,与VPO一起形成物理地址。
图67 PPN和VPO解析
7.5 三级Cache支持下的物理内存访问
当MMU产生了物理内存后,就需要去访问缓存如果该物理地址在L1中命中,则直接读出数据;否则依次从L2,L3和主存中读取。如果都没有命中,从磁盘中读取,并将数据缓存在三级Cache和主存中。
物理地址可以分为CT,CI,CO三个部分,分别为标记,组索引和块偏移。CPU利用组索引去寻找对应的Cache组,比较标记,只有当有效位和标记都满足条件的时候才能称为缓存命中,此时依据CO从Cache中取出数据返回。L1,L2,L3都不命中的情况下,才会访问内存,从内存中读取到缓存中然后返回。
图68 三级cache解析过程
7.6 hello进程fork时的内存映射
Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域,这个过程称为内存映射。Fork 创建新的进程之后,会为新的进程创建虚拟内存空间映射。两个内存空间具有相同的上下文。在内存写入时,文件映射内存 mmap和文件取消映射内存 mmap,在一个进程内执行不会影响到其他进程。
7.7 hello进程execve时的内存映射
在执行hello时,shell会调用execve(“./hello”.NULL,NULL)函数语句,这个函数语句执行以下步骤:
(1)删除已存在的用户区域
(2)映射私有区域
(3)映射共享区域
(4)设置程序计数器到当前代码入口点
7.8 缺页故障与缺页中断处理
缺页故障需要操作系统与硬件共同配合。如果操作系统尝试访问一个虚拟地址,它会向MMU发送请求,此时MMU触发了缺页,这会使得操作系统异常控制转移到缺页处理程序,随后进行如下步骤:
图69 缺页故障处理过程
(1)处理器生成一个虚拟地址,传送给MMU
(2)MMU生成页表条目地址,并从高速缓存/主存请求得到它
(3)高速缓存/主存向MMU返回页表条目
(4)PTE中的有效位是0,所以MMU发出了一次异常,控制权传递到CPU中的缺页异常处理程序进行异常处理
(5)缺页处理程序确认物理内存中的牺牲页,如果这个页已经被修改了,则把它写回到磁盘。(注意写回策略决定写回时机)
(6)缺页处理程序页面调入新的页面,并更新内存中的页表条目
(7)命令重新执行,此时缺页异常已经解决
7.9动态存储分配管理
在程序运行时程序员使用动态内存分配器 (比如 malloc) 获得虚拟内存。动态内存分配器维护者一个进程的虚拟内存区域,称为堆。
分配器将堆视为一组不同大小的 块(blocks)的集合来维护,每个块要么是已分配的,要么是空闲的。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。C语言使用的是显示分配器,它要求程序员手动free申请的内存;而Java等语言使用的是隐式分配器,其内存回收使用的是GC。
7.10本章小结
本章介绍了hello程序的存储管理,介绍了存储器地址空间、段式管理、页式管理等等存储器管理概念,加深了我们对于进程运行时存储情况的理解。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个字节序列,所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。Linux就是基于Unix I/O实现对设备的管理,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
8.2.1 Unix I/O 接口的几种操作:
1.打开文件:程序要求内核打开某文件时,内核返回一个小的非负整数(文件描述符)用于在后续操作中标识这个文件。
2. 改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。
3. 读写文件:一个读操作就是从文件复制 n>0 个字节到内存,同时把文件位置从k调整到k+n。给定一个大小为m字节的文件,当 k>=m 时执行读操作会出发一个称为 EOF 的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的EOF符号,但是可以标识文件结束。
4.关闭文件:关闭文件是通知内核你要结束访问一个文件,内核释放打开文件时创建的数据结构以及占用的内存资源,并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
8.2.2 Unix I/O函数
int open(char* filename, int flags, mode_t mode);
进程通过调用 open 函数来打开已存在的文件或是创建新文件。
int close(int fd);
该函数可以关闭一个打开的文件,其中fd是需要关闭的文件的描述符。
size_t write(int fd, const void *buf,size_t n);
该函数从内存位置复制至多n个字节到描述符为fd的当前文件位置。
size_t read(int fd, void *buf, size_t n);
该函数从描述符为fd的文件位置赋值最多n个字节到内存位置。返回-1表示出现错误,0表示EOF;否则返回值表示的是实际传送的字节数量。
8.3 printf的实现分析
8.3.1源码:
printf:
1.int printf(const char *fmt, ...)
2.{
3.int i;
4.char buf[256];
5.
6.va_list arg = (va_list)((char*)(&fmt) + 4);
7.i = vsprintf(buf, fmt, arg);
8.write(buf, i);
9.
10.return i;
11.}
vsprintf:
1.int vsprintf(char *buf, const char *fmt, va_list args)
2. {
3. char* p;
4. char tmp[256];
5. va_list p_next_arg = args;
6.
7. for (p=buf;*fmt;fmt++) {
8. if (*fmt != '%') {
9. *p++ = *fmt;
10. continue;
11. }
12.
13. fmt++;
14.
15. switch (*fmt) {
16. case 'x':
17. itoa(tmp, *((int*)p_next_arg));
18. strcpy(p, tmp);
19. p_next_arg += 4;
20. p += strlen(tmp);
21. break;
22. case 's':
23. break;
24. default:
25. break;
26. }
27. }
28.
29. return (p - buf);
30.}
8.3.2 源码分析
所有的参数在格式化之后经过vsprintf函数存入buf数组,并返回格式化数组的长度。printf中的write函数将i个元素写到终端。整个过程经历了从vsprintf生成待显示的信息,到系统函数调用(write),到陷阱(int 0x80或syscall)。
关于字符显示驱动子程序:从ASCII显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),之后每个点对应显示。
8.4 getchar的实现分析
getchar是 stdio.h 中的库函数,它的作用是从 stdin 流中读入一个字符,如果缓冲区内还有字符,那么就不需要用户的输入就可以读取了。
当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数将用户输入的字符回显到屏幕。如果用户在按回车之前输入了不止一个字符,多余的字符会保留在键盘缓存区中,等待后续getchar调用读取。所以,本质上,按回车就是把用户输入的字符串存入缓冲区中,等待之后使用。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了 Linux 的 I/O 设备管理方法,以及Unix I/O 接口及其函数。然后通过对printf 函数和 getchar 函数的工作过程的分析,加深了对Unix I/O的理解。
结论
(1)hello.c的源代码开始以文件的形式存放在硬盘中
(2)对hello.c进行预处理,编译,汇编,链接,生成一个可执行程序hello
(3)在bash等shell中输入./hello运行程序,shell调用fork,execve等函数创建子进程,映射各部分,加载程序文件,进入程序入口点,开始执行文件。
(4)当程序执行结束后,shell父进程(或者init)回收终止的子进程hello,清理hello所占用的资源,关闭文件描述符等等
一个简简单单的hello程序,背后竟然有着如此精妙的多部分协同,还有很多值得我们探寻的东西,我想这就是计算机的魅力所在…
附件
| 文件名 | 作用 |
| hello.i | hello.c预处理后生成的文件 |
| hello.s | 编译生成的汇编文件 |
| hello.o | hello.s生成的可重定位目标文件 |
| hello | hello.o链接之后生成的可执行文件 |
| hello.elf | hello.o由readelf命令生成的elf文件 |
| hello2.elf | hello生成的elf文件 |
| hello_o_information.txt | hello.o由objdump生成的反汇编代码 |
| hello_information.txt | hello由objdump生成的反汇编代码 |
参考文献
[1] zsh中奇怪的“工作”行为 zsh中奇怪的“工作”行为 | 码农俱乐部 - Golang中国 - Go语言中文社区
[2] Linux Shell 内建命令 : fg , bg , jobs 命令详解
Linux Shell 内建命令 : fg , bg , jobs 命令详解_HarkerYX的博客-CSDN博客
[3] 程序的不同段的区别和作用
程序的不同段的区别和作用_Quartz010的博客-CSDN博客



