计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 信息安全
学 号 2021111602
班 级 2103201
学 生 刘宇昂
指 导 教 师 刘宏伟
计算机科学与技术学院
2023年5月
本篇论文介绍了hello.c程序从预处理、编译到汇编、链接的过程,其中涉及到进程管理、存储管理、I/O管理等诸多问题,完整地展示了hello.c程序从生成到被回收的全部过程。本论文立足于hello.c程序,旨在见微知著,展现计算机对程序进行处理的全部过程。
关键词:预处理、编译、汇编、链接、进程管理、存储管理、I/O管理
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 6 -
3.2 在Ubuntu下编译的命令............................................................................. - 8 -
4.2 在Ubuntu下汇编的命令........................................................................... - 11 -
5.2 在Ubuntu下链接的命令........................................................................... - 15 -
5.3 可执行目标文件hello的格式.................................................................. - 16 -
5.5 链接的重定位过程分析............................................................................... - 18 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 21 -
6.3 Hello的fork进程创建过程..................................................................... - 21 -
6.6 hello的异常与信号处理............................................................................ - 22 -
第7章 hello的存储管理............................................................................... - 26 -
7.1 hello的存储器地址空间............................................................................ - 26 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................ - 26 -
7.3 Hello的线性地址到物理地址的变换-页式管理...................................... - 26 -
7.4 TLB与四级页表支持下的VA到PA的变换............................................. - 27 -
7.5 三级Cache支持下的物理内存访问.......................................................... - 28 -
7.6 hello进程fork时的内存映射.................................................................. - 28 -
7.7 hello进程execve时的内存映射.............................................................. - 28 -
7.8 缺页故障与缺页中断处理........................................................................... - 28 -
8.1 Linux的IO设备管理方法.......................................................................... - 31 -
8.2 简述Unix IO接口及其函数....................................................................... - 31 -
第1章 概述
1.1 Hello简介
P2P(From Program to Process)过程:
Hello.c的P2P过程大体可分为四个阶段:预处理阶段、编译阶段、汇编阶段、链接阶段。预处理阶段通过预处理器,把源程序处理为修改之后的源程序hello.i;编译阶段通过编译器编译,将hello.i程序编译为汇编程序hello.s;汇编阶段则使用汇编器生成重定位目标文件hello.o;最后在链接阶段经由连接器与printf.o文件链接,生成可执行文件hello。系统会为它创建一个新进程,实现由程序到进程的转化。
图1-1-1 hello.c流程
O2O(From Zero-0 to Zero-0)过程:
在运行过程中,系统通过shell调用execve函数,将hello程序加载到当前进程的上下文中,为程序分配物理内存,进行虚拟内存的映射。当程序运行结束后,shell会作为父进程负责将其回收,删除程序相关数据,释放物理内存。
1.2 环境与工具
(1)硬件信息
图1-2-1 硬件配置
(2)软件环境
Windows 11 64位;VMware Workstation Pro;Ubuntu 20.04
(3)使用工具
gdb;edb;readelf;objdump;Code::Blocks20.03
1.3 中间结果
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行文件
hello:链接之后的可执行文件
hello0.elf:hello.o的ELF格式
hello.elf:hello的ELF格式
hello0.txt:hello.o反汇编代码
hello.txt:hello的反汇编代码
1.4 本章小结
本章介绍了hello.c程序的P2P及O2O过程,给出了实验进行的环境配置及实验过程中生成的中间结果。
第2章 预处理
2.1 预处理的概念与作用
预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置。
当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
2.2在Ubuntu下预处理的命令
使用gcc hello.c -E -o hello.i指令对hello.c程序进行预处理。
图2-2-1 预处理命令
2.3 Hello的预处理结果解析
使用文本编辑器打开hello.i程序,对所得结果进行分析:
图2-3-1 hello.i 程序
hello.i程序相对于hello.c进行了相对巨大的扩展,将原本的hello.c中的库文件与内部函数进行了展开,方便进一步的编译处理工作,而原本的代码则被保留在了文件的最后。
2.4 本章小结
本章主要介绍了预处理的概念及作用,同时生成hello.i文件,与原文本hello.c进行比较,分析预处理阶段进行的工作。
第3章 编译
3.1 编译的概念与作用
编译的概念:
编译是指利用编译器ccl从预处理文件(.i文件)产生汇编程序(.s文件)的过程,其中包含词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成这六个阶段。
编译的作用:
将文本文件hello.i翻译成文本文件hello.s。
3.2 在Ubuntu下编译的命令
编译的命令:gcc -s hello.i -o hello.s
图3-2-1 编译命令
3.3 Hello的编译结果解析
3.3.1 局部变量i
图3-3-1 局部变量
通过movl指令为局部变量int i在栈上分配空间。
3.3.2 字符串常量
图3-3-2 字符串常量
printf中函数中的字符串在main函数之前处理,表明它被作为局部的字符串常量处理。
3.3.3 数组
图3-3-3 数组
main函数中的char*argv[]数组被储存在栈中,可见,它里面的每一个元素都是一个指向字符类型数据的指针。
3.3.4 赋值操作
图3-3-4 赋值操作
hello.c中为i进行了赋值操作。
3.3.5 算术操作
图3-3-5 算术操作
hello.c中的i++,通过addl指令实现。
3.3.6 关系判断及转移操作
图3-3-6 关系判断及转移操作
hello.c中,此处判断i与8的大小关系,如果i大于等于8则进行转移
3.3.7 函数调用操作
图3-3-7 函数调用操作
通过call指令,实现对printf、getchar等函数的调用。
3.4 本章小结
本章实现了从hello.i到hello.s的编译过程,生成汇编代码,有助于进一步的汇编与链接操作,是hello.c程序执行过程中不可缺少的环节。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:
汇编是指汇编器as将.s文件翻译成机器语言指令,并将这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中的过程。
汇编的作用:
完成从汇编语言文件到可重定位目标文件的转化过程,便于进一步的链接过程。
4.2 在Ubuntu下汇编的命令
汇编命令:gcc -c hello.c -o hello.o
图4-2-1 汇编命令
4.3 可重定位目标elf格式
使用命令:readelf -a hello.o > hello0.elf,将hello.c写入到elf文件hello0.elf中。
(1)ELF头
图4-3-1 ELF头
ELF头以一个16字节的序列(Magic,魔数)开始,这个序列描述了生成文件的系统的字的大小和字节顺序。其余部分则介绍了该文件的类别、数据、类别、系统架构、标志等内容。
(2)节头表
图4-3-2 节头表
节头表中包含了各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐等信息。
(3)重定位节
图4-3-3 重定位节
重定位节中包含.text节中需要被修正的信息,可以看到.rodata、puts、exit、printf、atoi、sleep、getchar符号的偏移,这会指导链接器进行下一步的链接工作。
4.4 Hello.o的结果解析
执行命令objdump -d -r hello.o>hello0.txt,将hello.o的反编译结果保存在文件hello0.txt中。
图4-4-1 hello0.txt
将该文件与hello.s进行比较:
1)分支跳转
hello.s的分支跳转会用助记符(.L3等)表示要转移到的位置,而hello.o则用实际的偏移地址来表示转移的目的地址。
图4-4-2 hello.o的分支跳转
2)函数调用
hello.s的函数调用会用函数的名称指代要转移到的位置,而hello.o则直接用计算出的地址表示实际的转移地址。
图4-4-3 hello.o的函数调用
3)立即数格式
hello.s采用十进制数来表示立即数,而hello.o采用十六进制数来表示。
图4-4-4 hello.o的立即数格式
4.5 本章小结
本章介绍了汇编阶段产生的可执行文件hello.o,对其产生的elf格式文件进行分析,并将其与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.3 可执行目标文件hello的格式
使用命令readelf -a hello > hello.elf将hello写入文件hello.elf中。
1)ELF头
图5-3-1 ELF头
hello的ELF头中,包含着magic、类别、数据等基本信息。
2)节头
图5-3-2 节头
节头中包含每个节的编号、名称、类型、地址、偏移量、大小等基本信息。
3)程序头
图5-3-3 程序头
程序头中包含着段的类型、偏移量、物理地址、虚拟地址等内容。
5.4 hello的虚拟地址空间
图5-4-1 EDB查看虚拟空间
通过EDB查看hello可知,hello的虚拟地址空间为0x401000-0x402000。
与5.3中的ELF文件进行比较,发现.init节的位置在0x401000处,刚好与EDB查询到的起始位置相同。
图5-4-2 .init节的位置
又如.text节的位置在0x4010f0处,在EDB中也能找到对应的内容。
图5-4-3 .text节的位置
5.5 链接的重定位过程分析
执行指令objdump -d -r hello > hello.txt
图5-5-1 链接的重定位
1)分支跳转
在hello.o中,分支跳转是采用偏移量的方式进行跳转的,而在hello .out中,分支跳转时采用直接跳转到具体的地址实现的。
图5-5-2 hello.out的分支跳转
2)函数调用
在hello.o中,函数的调用时直接用当前的下一个地址占位,而在hello.out文件中是跳转到[函数名@plt]的函数的地址处。
图5-5-2 hello.out的函数调用
3)数据访问
在hello.o中,对rodata节的访问跳转用0x0占位,而在hello.out中,则给出了具体的跳转长度。
图5-5-3 hello.out的数据访问
5.6 hello的执行流程
执行流程:
1)开始执行时调用的函数:_start、_libc_start_main
2)执行main时调用的函数:_main、_printf、_exit、_sleep、_getchar
3)退出时调用的函数:exit
整个执行过程中运行的函数及其地址:
_start:0x4010f0
_libc_start_main:0x2f12271d
main:0x401125
_printf:0x401040
_exit:0x401070
_sleep:0x401080
_getchar:0x401050
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分为相互独立的部分,而在程序运行时才把它们链接在一起形成一个完整的程序,这样可以更好体现程序的灵活性,便于随时对程序进行修改。
在调用共享库函数时,由于编译器无法提前预测函数运行时地址,因此要为该引用生成一条重定位记录,再在程序加载时用动态链接器解析,这一过程在got节中实现。为了观察在引用前后程序的变化,我们需要关注got节发生的变化。
从elf文件中得知,got节的位置为0x404000。
图5-7-1 got节的位置
实际运行程序,观察到调用dl_init后,got节的内容发生了变化。
图5-7-2 运行前got节的内容
图5-7-3 运行后got节的内容
5.8 本章小结
本章介绍了链接的过程,对比了可执行文件hello.out与hello.o的区别,分析了在链接过程中链接器的操作,并对hello的执行流程和动态链接过程进行了分析。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
进程是计算机中的程序关于数据集合上的运行活动,既是系统进行资源分配和调度的基本单位,也是操作系统结构的基础。
进程的作用:
所有程序都运行在某个进程的上下文中,操作系统通过管理进程来实现对程序的正确执行。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:
Shell 是一个命令解释器,它负责为使用者提供操作界面,解释由用户输入的命令并且把它们送到内核,并且调用相应的应用程序。
Shell-bash的处理流程:
(1)终端进程读取用户从键盘输入的命令行。
(2)shell通过分析命令行字符串,获取到命令行参数,并构造传递给execve的argv向量。
(3)检查第一个命令行参数是否是一个内置的shell命令。如果不是内部命令,调用fork( )创建新进程/子进程;如果是内部命令,则直接调用相应的程序进行处理。
(4)在子进程中调用execve(执行指定程序,execve的参数从之前获取到的命令行参数中取得。
6.3 Hello的fork进程创建过程
当在终端中输入./hello 学号 姓名 秒数的指令后,终端对输入的命令行进行解析,当发现这不是一个内置命令时,终端会调用fork函数创建一个新的运行的子进程。
子进程将会得到与父进程完全相同但是彼此独立的副本,同时获得与父进程任何打开文件描述符相同的副本。
6.4 Hello的execve过程
进程创建成功后,系统调用execve将一个可执行文件加载到当前进程的上下文中,替换掉原来的程序。execve实际上并不会直接把可执行文件的内容复制到物理内存,而是使用了内存映射机制,代码和数据区直接映射到可执行文件中的.text、.data、.rodata等节。.bss、栈、堆都将映射到匿名文件。在这之后,execve会将参数列表argv[]以及环境变量列表envp[]传递给加载的程序,程序可以使用带参数的main接受它们。
6.5 Hello的进程执行
1.上下文切换:如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,这就是上下文切换的过程。
2.调度:加载保存的寄存器,切换到虚拟地址空间。
在进程调用execve函数之后,进程会为hello程序分配了新的虚拟的地址空间。最初,hello在用户模式下运行,输出内容,然后调用sleep函数进程进入内核模式,运行信号处理程序,信号处理程序运行完毕后再返回用户模式。
运行过程中,cpu不断切换上下文,使运行过程被切分成许多时间片,与其他进程交替占用cpu,实现进程的调度过程。
图6-5-1 进程的切换过程
6.6 hello的异常与信号处理
异常共分为以下四种:
图6-6-1 异常的分类
常见的信号有以下几种:
图6-6-2 信号的种类
在正常运行的情况下,程序会正常输出学号、姓名等信息。
图6-6-3 正常运行
在程序运行的时候键入Ctrl+C,hello会直接结束,这是因为Ctrl+C会发送一个SIGINT信号给hello,该信号会直接终止进程:
图6-6-4 终止进程
按下Ctrl+Z:进程收到 SIGSTP 信号,hello 进程会暂时挂起。
图6-6-5 暂停进程
此时可以输入指令ps查看后台进程,jobs指令查看当前处于暂停状态的进程,fg指令使进程继续,kill指令杀死进程。
图6-6-6 暂停时执行的指令
尝试在进程执行过程中输入回车或乱按,发现不会对程序的运行产生影响。
图6-6-7 进程执行时尝试干扰
6.7本章小结
在本章中,着重于hello.c程序运行过程中的进程管理,介绍了shell的运行机制及fork,execve函数的工作,以及异常处理和信号处理程序的运行规则。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1、逻辑地址:程序经过编译后出现在汇编代码中的地址,在这里指的是hello.o中的地址。
2、线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,线性地址表现为“描述符:偏移量”的形式,在hello程序中用逻辑地址加上程序中给出的偏移量即为线性地址。
3、虚拟地址:与线性地址相同。
4、物理地址:CPU通过地址总线寻址,找到物理内存对应的真实存在的地址,在hello程序中指其虚拟地址映射到的实际上的物理内存对应的地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。如果通过selector在全局描述符表中获取到段基址,再与段内偏移相加,就得到了线性地址。这个过程就被称作段式内存管理。
7.3 Hello的线性地址到物理地址的变换-页式管理
页表是一个包含许多页表条目(PTE)的数组,虚拟地址空间中的每个页在页表中都有一个PTE。PTE由一个有效位和一个n位地址字段组成,有效位表明该虚拟页是否被缓存在DRAM中。如果设置了有效位,那么地址字段表示相应的物理页的起始位置;如果没有设置有效位,那么空地址表示虚拟页还未被分配,否则这个地址指向该虚拟页在磁盘的起始位置。
MMU利用页表实现从虚拟地址到物理地址的变换。n位的虚拟地址包含一个p位的虚拟页面偏移VPO和一个n-p位的虚拟页号VPN。MMU利用VPN选择适当的PTE,如果这个PTE设置了有效位,则页命中,将页表条目中的物理页号和虚拟地址中的VPO连接起来就得到相应的物理地址。否则会触发缺页异常,控制传递给内核中的缺页异常处理程序。缺页处理程序确定物理内存中的牺牲页,调入新的页面,并更新内存中相应PTE。处理程序返回到原来的进程,再次执行导致缺页的指令,MMU重新进行地址翻译,此时和页命中的情况一样。同时,也可以利用TLB缓存PTE加速地址的翻译。
图7-3-1 MMU的结构
7.4 TLB与四级页表支持下的VA到PA的变换
处理器生成一个虚拟地址,并将其传送给MMU。MMU用VPN向TLB请求对应的PTE,如果命中,则跳过之后的几步。MMU生成PTE地址(PTEA).,并从高速缓存/主存请求得到PTE。如果请求不成功,MMU会向主存请求PTE,高速缓存/主存向MMU返回PTE。PTE的有效位为零, 此时会触发缺页异常,缺页处理程序确定物理内存中的牺牲页。缺页处理程序调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来进程,再次执行导致缺页的指令。
在四级页表下,会多次执行该流程,直到找到虚拟地址对应的物理地址为止。
图7-4-1 虚拟页向物理页的映射
7.5 三级Cache支持下的物理内存访问
在三级Cache支持下,CPU在访问物理内存前首先会查询Cache。首先确定L1中是否已缓存需要访问的内存,该块是否有效,如果已被缓存且有效就称为Cache命中,直接就对其进行读写即可,如果Cache L1没有找到,则以此类推向L2,L3中查询,如果依旧没有,才会访问内存,访问时需要把目的数据附近的整个Cache line长度的数据都加载进入Cache L3,L2,L1中后再进行读写操作。
7.6 hello进程fork时的内存映射
在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。通过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,这样就为每个进程保持了私有地址空间。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:
删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构。
映射共享区域,将hello程序与共享对象 libc.so 链接,然后再映射到用户虚拟地址空间中的共享区域内。
设置程序计数器,execve需要设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障:
当虚拟内存中的字不在物理内存中即为缺页。若CPU尝试引用某个字,但该字并未缓存在主存中。此时当地址翻译硬件从内存中读取该字时,会从有效位推断出它并未被缓存,并且触发一个缺页异常。
缺页中断处理:
缺页处理程序是系统内核中的代码,选择一个牺牲页,如果这个牺牲页被修改过,那么就将它交换出去,换入新的页并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令会再次发送VA到MMU。
图7-8-1 缺页的处理方法
7.9动态存储分配管理
动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆,系统之间细节不同,但是不失通用性,对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个个连续的虚拟内存片,要么是已分配的,要么是空闲的。一个已分配的块会保持已分配状态,直到它被释放。
分配器有两种基本风格。两种风格都要求应用显示地分配块。他们的不同之处在于由哪个实体来负责释放已分配的块。
1.显示分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
2.隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
7.10本章小结
本章详细介绍了hello程序执行过程中包括地址变换、虚拟地址到物理地址的转变、cache与页表的访问原理、fork与execve函数的内存分配等内容,较为详细地介绍了hello的存储管理过程。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux将文件所有的I/O设备都模型化为文件,甚至内核也被映射为文件。这种将设备映射为文件的方式被称为Unix I/O。
8.2 简述Unix IO接口及其函数
Unix IO接口有着以下的功能:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个较小的非负整数,称作描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,将它初始化为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
3.读写文件。读操作就是从文件复制n>0个字节到内存,复制的位置从当前文件位置k开始,连续复制n个字节,给定一个大小为m字节的文件,当k>=m时,触发EOF。
4.关闭文件。当应用完成了对文件的访问之后,它就会通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。
常用Unix I/O函数:
①int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
②int close(fd)
进程通过调用close函数关闭一个打开的文件,fd是需要关闭的文件的描述符。
③ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
④ssize_t wirte(int fd,const void *buf,size_t n)
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;
}
引用的vprintf函数:
int vsprintf(char *buf, const char *fmt, va_list args)
{
char* p;
char tmp[256];
va_list p_next_arg = args;
for (p=buf;*fmt;fmt++) {
if (*fmt != '%') {
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt) {
case 'x':
itoa(tmp, *((int*)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
}
return (p - buf);
}
vsprintf函数将所有的参数内容格式化之后存入buf,并返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall,之后由ASCII进入到字模库生成vram。显示芯片按照刷新频率逐行读取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实现的。getchar通过read函数从缓冲区读入一行,并返回读入的第一个字符。若读入失败,则返回EOF。
异步异常-键盘中断的处理:当用户通过键盘输入时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得扫描码,再将其转换为ASCII码,保存到系统的键盘缓冲区中。
8.5本章小结
本章介绍了unix的I/O管理内容,并分析了unix接口及其调用的函数,并将printf和getchar函数的实现进行了详细的分析。
结论
hello函数在执行过程中经历了以下步骤:
预处理:对hello.c进行预处理,将c文件调用的库全部展开合并得到到hello.i文本文件;
编译:将hello.i编译生成汇编语言文件hello.s;
汇编:将hello.s汇编到二进制可重定位目标文件hello.o;
链接:将hello.o与可重定位目标文件和动态链接库链接生成可执行文件hello;
创建子进程:shell调用fork函数,创建子进程;
加载程序:shell调用execve函数,启动加载器,进行虚拟内存的映射,进入程序入口之后开始载入物理内存,然后进入main函数。
交互:hello的输入输出与外界交互,在这里会用到unix I/O函数;
终止:hello程序终止后,会被作为父进程的shell回收。
经过本课程的学习,我了解到了计算机实际运行过程中各模块的内部原理,从简单的数据表示,汇编代码的执行,到程序的链接、内存分配,让我感叹计算机不愧是一种极其精密的机器,也坚定了我未来继续学习的决心。
从最初学习编程的hello.c程序入手,让我明白了每一个看似简单的程序都有着复杂且合理的运行过程,每一个过程都是在无数次尝试与优化中确立起来的,每一个从事计算机行业的先驱者与后继者都值得我们尊敬!
附件
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行文件
hello:链接之后的可执行文件
hello0.elf:hello.o的ELF格式
hello.elf:hello的ELF格式
hello0.txt:hello.o反汇编代码
hello.txt:hello的反汇编代码
参考文献
[1] Randal E.Bryant, David O’Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4
[2] 袁春风. 计算机系统基础. 北京:机械工业出版社,2018.7(2019.8重印)
[3] https://www.cnblogs.com/pianist/p/3315801.html