计算机系统大作业
摘 要
本文主要讲述了hello.c程序在编写完成后运行在linux中的生命历程,借助相关工具分析预处理、编译、汇编、链接等各个过程在linux下实现的原理,分析了这些过程中产生的文件的相应信息和作用。并介绍了shell的内存管理、IO管理、进程管理等相关知识,了解了虚拟内存、异常信号处理等相关内容。
关键词:预处理;编译;汇编;链接;进程;存储管理;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简介
P2P(From Program to Process)过程:
使用高级语言(C语言)保存生成.c文件,通过语言预处理器(cpp)生成.i文件,再通过编译器(cc1)生成.s文件,再通过汇编器(as)生成可执行文件(hello)。通过shell执行该文件,shell会调用fork,execve函数为hello产生新进程。
020(From Zero-0 to Zero-0)过程:
在调用execve并加载运行hello的过程中,会删除已存在的用户区域、映射私有区域、映射共享区域、设置程序计数器,并将参数argv[]与环境变量envp[]传给新进程的main函数。新进程执行完毕后,父进程(shell)回收子进程,删除相关数据结构。
1.2 环境与工具
1.2.1 硬件环境
X64 Intel® Core™ i7-8650U CPU; 16.0GB RAM;256GB SSD
1.2.2 软件环境
Windows 10
Ubuntu 18.04
1.2.3 开发工具
CodeBlocks 64位
VusualStudio 2019
1.3 中间结果
hello.i:预处理生成的文本文件
hello.s:编译后得到的汇编语言文件
hello.o 汇编后得到的二进制可重定位目标文件
hello 经过链接生成的可执行目标文件
1.4 本章小结
本章通过追踪hello程序的P2P、020过程,分析程序加载、执行回收的过程,并列出了实验环境。
第2章 预处理
2.1 预处理的概念与作用
预处理:编译之前处理
C语言的预处理只要有一下三方面的内容(预处理命令以符号#开头):
- 宏定义(#define):
宏定义又称宏替换、宏代换,将代码中所有在宏定义中定义的标识符
全都替换为相应的内容。 - 文件包含(#include ”filename” 或 #include ):
一个文件包含另一个文件的内容,将一个文件的全部内容复制到另一个文件中,允许嵌套包含,如(a.h包含b.h,b.h包含c.h),但不允许递归包含(如a.h包含b.h,b.h包含c.h)。 - 条件编译(#if #elif #else #ifdef #ifndef #endif等)
在很多情况下,我们希望程序的其中一部分代码只有在满足一定条件时才进行编译,否则不参与编译(只有参与编译的代码最终才能被执行)。因为条件编译是在编译之前进行,所以带参数的宏比函数具有更高的效率。使用条件编译可使目标程序变小,运行时间变短,使问题的解决方案增多,有助于我们选择合适的解决方案。
删除所有注释、添加行号及文件标识
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
图2-2-1 Ubuntu下的预处理命令
图2-2-2 hello.i中的内容
图2-2-3 hello.i中的内容
图2-2-4 hello.i中的内容
2.3 Hello的预处理结果解析
预处理会删除所有注释、添加行号以及文件标识。
预处理会处理所有条件编译指令。
预处理会将#include中包含的文件内容复制到.i文件中,其中可能会有循环嵌套。导致预处理完成后的.i文件大小远大于.c文件。
预处理会将代码中的在宏定义中定义过的符号进行替换。
2.4 本章小结
本章分析了预处理的过程并在Ubuntu演示了预处理过程。分析了预处理文件的信息。
第3章 编译
3.1 编译的概念与作用
编译:利用编译程序从预处理后的文件(.i)生成汇编语言程序(.s)。
编译时把高级语言编程较低级的汇编语言。主要包括以下五个阶段:
- 词法分析:
词法分析词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。 - 语法分析:
编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构。 - 中间代码
中间代码是源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码,即为中间语言程序,中间语言的复杂性介于源程序语言和机器语言之间。 - 代码优化
代码优化是指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。所谓等价,是指不改变程序的运行结果。所谓有效,主要指目标代码运行时间较短,以及占用的存储空间较小。 - 目标代码
目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。目标代码有三种形式:
① 可以立即执行的机器语言代码,所有地址都重定位;
② 待装配的机器语言模块,当需要执行时,由连接装入程序把它们和某些运行程序连接起来,转换成能执行的机器语言代码;
③ 汇编语言代码,须经过汇编程序汇编后,成为可执行的机器语言代码。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图3-2-1 Ubuntu下的编译命令
图3-2-2 hello.s 中的内容
图3-2-3 hello.s中的内容
3.3 Hello的编译结果解析
3.3.1汇编文件指令
指令 作用
.file 以下是代码段
.text 以下是数据段
.section .rodata 以下是rodata节内容
.globl 全局变量
.type 指定是函数类型还是对象类型
.size 声明大小
.long .string 声明long、string类型
.align 声明指令数据对其方式
3.3.2 数据类型
- 变量int i、int argc
局部变量分配在栈中,没有标识符。
可以看出hello.s将i存储在-4%(%rbp)(参考addl $1, -4(%rbp))。
argc作为第一个参数,有寄存器%edi保存,然后又被存入-20(%rbp)。
图3-3-1 局部变量的汇编代码
2. 字符串
两个printf函数中包含两个字符串字面量,为只读数据存储在.rodata段。
图3-3-2 字符串常量
3. 数组
指针数组变量char * argv[]存储参数的地址。printf函数调用了argv[1]和argv[2]。系统通过(%rbp-32)得到argv[0]的指针的地址、+8得到argv[1]的地址,+16得到argv[2]的地址。
3.3.3 赋值
代码中的复制语句:i=0;
图3-3-3 赋值语句
3.3.4 算数操作
- for循环中有递增操作i++
图3-3-3 i++递增操作
3.3.5 关系操作
程序中的涉及到的关系操作:
1.
图3-3-4 C语言代码中的关系操作1
图3-3-5 汇编代码中的关系操作1
汇编代码中的cmpl与je判定了argc!=4时执行下面的语句。
2.
图3-3-6 C语言代码中的关系操作2
图3-3-6 汇编代码中的关系操作2
3.3.6 控制转移
在开头的cmpl和紧接着的je指令组合:当argc=5时,程序跳转到.L2。而如果argc不等于4时,程序跳过je .L2继续依次执行剩下的指令。正好对应着if判断句不成立时程序将执行{}里的程序
在for循环中,遇到了cmpl+jle组合,依旧是判断i是否小于等于8,如果是就跳转到.L4,即重新执行上述操作直到i=10时,程序再次进入.L3后判断条件不成立,程序跳过jle .L4执行下一条语句,即跳出循环。
3.3.7 函数操作
图3-3-7 汇编代码中的函数传参
函数调用前程序会使用寄存器存储参数,若寄存器大于6个则使用栈传参。传参寄存器的顺序为%rdi、%rsi、%rdx、%rcx、%r8、%r9
在执行call语句时先讲当前地址入栈,跳转到待执行函数指令处。函数返回,执行ret将地址出栈,返回值存储在%rax(%eax)中。
图3-3-8 函数返回
3.4 本章小结
高级语言汇编语言,可以将不同种类的高级语言在执行过程中统一化,汇编语言是对更加难以理解的机器语言的抽象化,覆盖了基本操作,但难以理解。
本章分析了编译过程中的具体操作,对不同类型的数据结构的存储与处理。
第4章 汇编
4.1 汇编的概念与作用
汇编:把汇编语言书写的程序翻译成与之等价的机器语言程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。汇编语言是为特定计算机或计算机系列设计的一种面向机器的语言,由汇编执行指令和汇编伪指令组成。
4.2 在Ubuntu下汇编的命令
图4-2-1 Ubuntu下的汇编指令
gcc –c –o hello.o hello.s
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
图4-3-1Elf头
Elf 头以一个16字节序列开始,描述了生成该文件的系统的字的大小和字节顺序,剩下的信息帮助链接器语法分析和解释目标文件的信息。包括ELF头大小,目标文件类型,机器类型,节头部表的文件偏移以及节头部表中条目的大小和数量。
图4-3-2节头部表
节头部表记录了各节名称、类型、地址、偏移量、大小、全体大小、旗标、连接、信息、对齐信息。
图4-3-3 重定位节
重定位节:.rela.text,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。调用本地函数的指令则不需要修改。链接器会依据重定向节的信息对可重定向的目标文件进行链接得到可执行文件。
偏移量:指需要进行重定向的代码在.text或.data节中的偏移位置,8个字节。
信息:包括symbol和type两部分,其中symbol占前4个字节,type占后4个字节,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型
类型:重定位到的目标的类型
符号名称:重定向到的目标的名称
加数:计算重定位位置的辅助信息,共占8个字节
图4-3-4符号表
符号表symtab存放着程序中定义和引用函数和全局变量的信息。
4.4 Hello.o的结果解析
图4-4-1hello.o的反汇编命令
使用命令objdump -d -r hello.o > hellores.txt获得反汇编代码
在汇编代码中,分支跳转是直接以.L0等助记符表示,但是在反汇编代码中,分支转移表示为主函数+段内偏移量。反汇编代码中的跳转分为直接寻址与间接寻址。
函数调用:汇编代码中函数调用时直接跟函数名称,而在反汇编的文件中call之后加main+偏移量(定位到call的下一条指令)。在.rela.text节中为其添加重定位条目等待链接。
访问全局变量:汇编代码中使用.LC0(%rip),反汇编代码中为0x0(%rip)。因为访问时需要重定位,所以初始化为0并添加重定位条目。
4.5 本章小结
本章分析了汇编的过程。通过汇编文件和elf格式了解.o文件的结构与信息,与.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 Ubuntu下的链接指令
5.3 可执行目标文件hello的格式
使用readelf -a hello>hello_elf.txt生成含有ELF信息的文本文件
图5-3-1 链接后的ELF头
图5-3-2 链接后的节头部表
图5-3-3 链接后的程序头
5.4 hello的虚拟地址空间
图5-4-1用edb加载hello
其中Symbols窗口中显示了hello程序中各段的名称和其起始地址对应了图5-3-2、图5-3-3中的名称和地址。而图5-3-3左下角的Data Dump显示的则是数据段的详细信息。而左上窗口显示的就是实际的程序里对应地址里的信息,相当于是图5-3-2、图5-3-3里段的对应地址里的详细内容。
5.5 链接的重定位过程分析
使用指令objdump -d -r hello >hello_obj生成重定位文件信息
图5-5-1重定位信息
对比可知hello与hello.o不同如下:
1.hello将hello.o中的待修改地址修改后,这些地址成了实际虚拟内存中的地址,并指向各个被调用的函数。
2.hello相对hello.o链接了许多库函数。
3.hello相对hello.o添加了节:.init,.plt等
4.hello.o中跳转以及函数调用的地址在hello中都被更换成了虚拟内存地址。
5.6 hello的执行流程
图5-6-1 使用edb加载hello
图5-6-2 hello的函数名及地址
5.7 Hello的动态链接分析
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章介绍了链接的概念与作用、hello的ELF格式,hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
作用:进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
作用:是一种交互型的应用级程序,时Linux的外壳,提供了一个界面,用户可以通过这界面访问操作系统内核。
流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序为其分配子进程并运行
5)shell应该接受键盘输入信号,并对这些信号进行相应处理
图6-2-1shell_bash的处理流程
6.3 Hello的fork进程创建过程
在终端中输入命令后,shell会处理该命令,判断出不是内置命令,则会调用fork函数创建一个新的子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本。
6.4 Hello的execve过程
fork之后子进程调用execve函数在当前进程的上下文中加载并运行一个新程序即hello程序。execve加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp,并将控制传递给main函数。
6.5 Hello的进程执行
逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。
上下文切换:上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。
开始Hello运行在用户模式,收到信号后进入内核模式,运行信号处理程序,之后再返回用户模式。运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
6.6 hello的异常与信号处理
运行过程中可能出现的异常种类由四种:中断、陷阱、故障、终止。
中断:来自I/O设备的信号,异步发生。硬件中断的异常处理程序被称为中断处理程序。
陷阱:是执行一条指令的结果。调用后返回到下一条指令。
故障:由错误情况引起,可能能被修正。修正成功则返回到引起故障的指令,否则终止程序。
终止:不可恢复,通常是硬件错误,这个程序会被终止。
图6.6.1正常运行
图6.6.2ctrl+c终止
图6.6.3ctrl+z暂停
6.7本章小结
本章介绍了进程的相关概念,描述了hello子进程fork和execve的过程,介绍了shell的一般处理流程和异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:格式为“段地址:偏移地址”,是CPU生成的地址,在内部和编程使用,并不唯一。
物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址。
虚拟地址:保护模式下程序访问存储器所用的逻辑地址。
线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
分段功能在实模式和保护模式下有所不同。
实模式:逻辑地址=线性地址=实际的物理地址。段寄存器存放真实段基址,同时给出32位地址偏移量,则可以访问真实物理内存。
保护模式:线性地址还需要经过分页机制才能够得到物理地址,线性地址也需要逻辑地址通过段机制来得到。
段寄存器用于存放段选择符,通过段选择符可以得到对应段的首地址。处理器在通过段式管理寻址时,首先通过段描述符得到段基址,然后与偏移量结合得到线性地址,从而得到了虚拟地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
7.4 TLB与四级页表支持下的VA到PA的变换
为减少时间开销,MMU中存在一个关于PTE的缓存,成为翻译后备缓冲器TLB。其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。
图7-4-1四级页表下寻址过程
7.5 三级Cache支持下的物理内存访问
1、CPU给出VA
2、MMU用VPN到TLB中找寻PTE,若命中,得到PA;若不命中,利用VPN(多级页表机制)到内存中找到对应的物理页面,得到PA。
3、PA分成PPN和PPO两部分。利用其中的PPO,将其分成CI和CO,CI作为cache组索引,CO作为块偏移,PPN作为tag。
先访问一级缓存,不命中时访问二级缓存,再不命中访问三级缓存,再不命中访问主存,如果主存缺页则访问硬盘
图7-5-1高速缓存层次结构
7.6 hello进程fork时的内存映射
Fork函数被调用是内核为hello新进程创建虚拟内存和各种数据结构并为其分配唯一的PID。为进行以上操作,还创建了当前进程的mm_struct、区域结构和样表的原样副本。Shell将两个进程中每个页面都标为只读,并将每个进程中的每个区域结构标记为写时复制。
7.7 hello进程execve时的内存映射
删除已存在的用户区域。
映射私有区域:为新程序的代码、数据、.bss和栈区域创建新的区域结构。
映射共享区:hello与系统执行文件链接映射到共享区域。
设置程序计数器PC:设置当前进程上下文中的PC,指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障是一种常见的故障,要访问的主页不在主存,需要操作系统调入才能访问。缺页中断处理函数为do_page_fault函数。
图7-8-1缺页故障操作图
7.9动态存储分配管理
分配器将堆视为一组不同大小的块(blocks)的集合来维护,每个块要么是已分配的,要么是空闲的。分配器的类型有两种:
显式分配器:要求应用显式地释放任何已分配的块.例如,C语言中的 malloc 和 free
隐式分配器:应用检测到已分配块不再被程序所使用,就释放这个块。
图7-9-1 隐式空闲链表
图7-9-2 显示空闲链表
7.10本章小结
本章了解了存储器地址空间,段式管理和页式管理,介绍了动态内存分配和进程的创建及fork和execve时的内存映射,还介绍了缺页故障和缺页中断处理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Linux以文件的方式对I/O设备进行读写,将设备均映射为文件。对文件的操作,内核提供了一种简单、低级的应用接口,即Unix I/O接口。
所有输入输出都按以下一致的方式执行:
打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
Shell创建的每个进程都有三个打开的文件:标准输入(stdin),标准输出(stdout),标准错误(stderr)。
改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
具体实现函数及其参数要求:
打开文件:int open(char *filename,int flags,mode_t mode); open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,而mode参数指定了新文件的访问权限位。
关闭文件:int close(int fd); fd是需要关闭的文件的描述符,close返回操作结果。
读文件:ssize_t read(int fd,void *buf,size_t n) read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
写文件:ssize_t write(int fd,const void *buf,size_t n);
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
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;
}
其中va_list即char *
va_list arg = (va_list)((char*)(&fmt) + 4); 即将arg指向了第一个const参数。
vsprintf(buf, fmt, arg)接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。它最终会返回一个长度,即要打印出来的字符串的长度。 vsprintf生成显示信息,到write系统函数,陷阱系统调用int 0x80或syscall,字符显示驱动子程序实现从ASCII到字模库到显示vram,可以存储每一个点的RGB颜色信息,显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。如此之后,屏幕上就会显示出我们输入的信息了。
8.4 getchar的实现分析
getchar函数的函数体如下:
int getchar(void)
{
static char buf[BUFSIZ];
static char* bb=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;
}
return(–n>=0)?(unsigned char)*bb++:EOF;
}
在需要从键盘读入一个字符时,内核中断当前进程,控制权交给键盘中断处理程序。getchar函数中调用了read系统函数,通过系统调用读取按键ascii码,保存到系统的键盘缓冲区,直到接受到回车键才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及函数,分析了printf和getchar函数。
结论
hello程序的过程可总结如下:
1、编写代码:用高级语言写.c文件
2、预处理:从.c生成.i文件,将.c中调用的外部库展开合并到.i中
3、编译:由.i生成.s汇编文件
4、汇编:将.s文件翻译为机器语言指令,并打包成可重定位目标程序hello.o
5、链接:将.o可重定位目标文件和动态链接库链接成可执行目标程序hello
6、运行:在shell中输入命令
7、创建子进程:shell用fork为程序创建子进程
8、加载:shell调用execve函数,将hello程序加载到该子进程,映射虚拟内存
9、执行指令:CPU为进程分配时间片,加载器将计数器预置在程序入口点,则hello可以顺序执行自己的逻辑控制流
10、访问内存:MMU将虚拟内存地址映射成物理内存地址,CPU通过其来访问
11、动态内存分配:根据需要申请动态内存
12、信号:shell的信号处理函数可以接受程序的异常和用户的请求
13、终止:执行完成后父进程回收子进程,内核删除为该进程创建的数据结构
计算机系统从.c源文件到运行结束,涉及多个程序、多个硬件协作,各司其职。需要各个方面的程序员工程是共同协作,贡献自己的智慧。如果要写出高效率的程序也要充分考虑到各个层次的问题。
附件
hello.c C语言源程序
hello.i 预处理得到的文本文件
hello.s 编译得到的汇编程序
hello.o 汇编后生成的可重定位目标文件
hello 连接后生成的可执行目标文件
hello_elf.txt hello的elf文件
hello_obj.txt hello的elf文件
参考文献
[1] 兰德尔 E.布莱恩特,大卫 R.奥哈拉伦. 深入理解计算机系统. 机械工业出版社
[2] https://blog.csdn.net/z1162565234/article/details/80466842 C语言——预处理(宏定义、文件包含、条件编译)
[3]https://baike.baidu.com/item/%E9%A2%84%E5%A4%84%E7%90%86%E5%91%BD%E4%BB%A4/10204389?fr=aladdin 百度百科:预处理命令
[4] https://baike.baidu.com/item/%E7%BC%96%E8%AF%91/1258343?fr=aladdin 百度百科:编译