计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 120L022123
班 级 2003007
学 生 鹿鸣
指 导 教 师 吴锐
计算机科学与技术学院
2022年5月
本文通过hello程序的运行,分析了linux系统下一个进程从创建到终止的全过程,对这一过程中所涉及到的预处理,编译,汇编,链接,分析了系统在程序运行时的进程管理,存储管理,IO管理等内容。
关键词:预处理,编译,汇编,链接,进程管理,存储管理,IO管理
目 录
第1章 概述
1.1 Hello简介
- 首先通过文本编辑器编辑hello.c源程序
- 用预处理器通过hello.c文件生成hello.i文件
- 用编译器通过hello.i文件生成hello.s文件
- 用汇编器通过hello.s文件生成可重定位目标文件hello.o
- 用链接器将hello.o文件和其它所需的可重定位目标文件链接形成可执 行目标程序hello
- 在shell中输入./hello,调用fork()函数创建子进程,调用execve()加载 并执行hello。
- 处理器从内存中获取hello相应的代码和数据执行逻辑控制流
- 当hello进程终止后,由其父进程shell负责回收
1.2 环境与工具
硬件环境:
处理器:AMD Ryzen 7
软件环境:
Windows10 ;Ubuntu 20.04
开发与调试工具:
gcc,as,ld,edb
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c :源程序
hello.i :经过预处理的代码(源程序)
hello.s :汇编语言程序
hello.o :可重定位目标文件
hello :可执行目标文件
hello_obj :hello的反汇编代码
hello_objdump :hello.o的反汇编代码
1.4 本章小结
本章简述了hello的P2P,020的整个过程。对本次实验的开发环境与调试工具进行了列举。并给出了该过程中所产生的中间结果文件及其作用。
第2章 预处理
2.1 预处理的概念与作用
处理器根据以字符#开头的命令,修改原始的C程序。如hello.c中的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容, 并把它直接插人程序文本中。结果就得到了另一个C程序,通常是以.i作为文 件扩展名。
2.2在Ubuntu下预处理的命令
命令: gcc -E hello.c -o hello.i
经过预处理得到的文件,由原来的527byte变成了64.7KB,代码有23行增加到3000多行,其中包含了.c文件中包含的三个库文件stdio.h,stdlib.h和unistd.h的全部内容。
2.4 本章小结
本章介绍了预处理的概念和作用,并在linux系统中通过在终端输入命令对hello.c文件执行了预处理操作并进行了分析。
第3章 编译
3.1 编译的概念与作用
概念:编译器(ccl)将文本文件hello.i翻译咸文本文件hello.s,它包含一个汇编语言程序。
作用:汇编语言为不同高级语言的不同编译器提供了通用的输出语言。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.3.1汇编语言文件开头部分
.file "hello.c"
.text 文件名
.section .rodata 只读段
.align 8 对齐方式,对其边界为8
.string 字符串
.string "Hello %s %s\n" 字符串
.text 代码段
.globl 全局变量
.type 类型声明
3.3.2数据
- 参数argc根据汇编代码movl %edi, -20(%rbp)和cmpl $4, -20(%rbp)可知,它被存放在edi中,用于保存main函数传入的参数个数。
- 根据movl $0, -4(%rbp)和cmpl $7, -4(%rbp)可知循环变量整型int i就存放在 -4(%rbp)处,它占用四个字节的内存。
- 参数char *argv[]是传入主函数的参数列表,其中argv[0]中保存了文件名,汇编代码开头处定义了字符串.string "Hello %s %s\n",经过分析可知argv[1]和argv[2]就是此处的两个字符串,它们都是.rodata段中的只读数据。
3.3.3赋值
整个hello.c中只有for循环处for(i=0;i<8;i++)一处进行了赋值操作,将初值0赋给循环变量i。对应于汇编语句movl $0, -4(%rbp),将长度为四个字节的整型立即数赋给i。
3.3.4类型转换
汇编语句中的call atoi@PLT对应了.c文件中的sleep(atoi(argv[3]))一行,由于sleep函数所需的参数类型为整型,所以将字符串argv[3]转化成了整型。
3.3.5算术操作
汇编语句addl $1, -4(%rbp)是对循环变量i执行加1操作,add为加法指令,而后缀l代表操作的数据长度为四个字节,对应于i的类型——整型。
3.3.6关系操作
汇编语句cmpl $4, -20(%rbp),对应于.c文件中的if(argc!=4),执行该语句会设置条件码;汇编语句cmpl $7, -4(%rbp)对应于.c文件中的for(i=0;i<8;i++),是对循环变量的判断。执行以上两条语句会设置条件码。
3.3.7指针/数组
通过对汇编语句movq -32(%rbp), addq $16, %rax, addq $8, %rax %rax,addq $24, %rax的分析可知,每个指针所占内存空间大小为8个字节,它们构成了一个长度为4的指针数组,并被分配了连续的内存。argv[0]中保存了文件名,argv[1]和argv[2]是两个被printf输出的字符串参数,argv[3]则被函数atoi使用,将字符串转化为整数。
3.3.8控制转移
汇编语句cmpl $4, -20(%rbp),对应于.c文件中的if(argc!=4),执行该语句会设置条件码,据此判断是否执行je .L2进行跳转。
汇编语句cmpl $7, -4(%rbp)对应于.c文件中的for(i=0;i<8;i++),是对循环变量的判断,执行该语句会设置条件码,据此判断是否执行je .L4进行跳转。
3.3.9函数操作
main函数:
参数传递:传入参数argc和argv
函数调用:系统调用
printf函数:
参数传递:传入参数argv[1]和argv[2]
函数调用:在for循环中被多次调用
exit函数:
参数传递:传入参数1
函数调用:满足if语句时调用
sleep函数:
参数传递:传入参数为atoi函数的返回值
函数调用:在for循环中被多次调用
getchar函数:
参数传递:传入参数为atoi函数的返回值
函数调用:在for循环中被多次调用
atoi函数:
函数调用:在循环结束后调用
3.4 本章小结
本章通过在linux系统中通过在终端输入命令对hello.i文件执行了编译操作并进行了分析了生成的汇编语言程序的代码内容,对其中的数据,赋值,类型转换,算术操作,关系操作,数组,指针,控制转移,函数操及其对应的c指令对比分析。
第4章 汇编
4.1 汇编的概念与作用
汇编:汇编器将汇编语言文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件。
作用:将汇编语言文件翻译成机器语言指令。
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
在终端中输入命令readelf -a hello.o > hello.elf生成.elf文件
ELF Header:包括ELF头的大小、目标文件的类型、机器类型、节头部表的 文件偏移, 以及节头部表中条目的大小和数量。
节头表:包括节的名称,类型,地址,对齐方式,大小,全体大小,旗标, 偏移量等信息
重定位节:重定位节保存的是.text节中需要被修正的信息,包括偏移量类型, 权值,名称等信息
符号表:是源程序中出现的所有符号的列表,包括全局变量,函数名称的信 息,但不包含局部变量的信息
4.4 Hello.o的结果解析
使用objdump命令将可重定位目标文件hello.o反汇编得到汇编语言文件如图
和hello.s的不同:
- 分支转移
在反汇编文件中:
在汇编文件中:
反汇编文件的跳转使用立即数进行定位,因为汇编语言中的.L3只是一个符号,而可重定位目标文件采用相对地址进行跳转,没有汇编语言中的符号。
- 函数调用
在反汇编文件中:
在汇编文件中:
反汇编文件的函数调用使用可重定位条目进行定位。
4.5 本章小结
本章通过在linux系统中通过在终端输入命令对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.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
ELF header:
节头表:对 hello中所有的节信息进行了声明
重定位节:
符号表:
5.4 hello的虚拟地址空间
hello进程的虚拟地址空间信息如图所示:
在5.3所示的节头表中可以看到可执行目标文件中各节的起始位置和大小, 如.data节的起始位置是0x004010d2,.text节的起始位置为0x00401040,大小为 0x92,.symtab节的起始位置为0x00401190,大小为0x1b0等。
5.5 链接的重定位过程分析
hello的反汇编文件如图所示:
和hello.o的反汇编文件相比,hello.out的反汇编文件中多了.init,.plt,.text三个节,而hello.o的反汇编文件中只有一个.text节。
此外在hello.o的反汇编文件中,文件的地址是从0x000000开始的。而hello.out的地址是从0x401000开始的,并且第一个函数为<_init>,此外还有一些在连接过程中从库文件中获取的其他函数在内。
部分新增的函数:
5.6 hello的执行流程
执行main之前:
_init(0x401000)
_start(0x4010f0)
_libc_csu_init(0x4011c0)
执行main:
_main(0x401125)、
_printf(0x4010a0)、
_exit(0x4010d0)、
_sleep(0x4010e0)、
_getchar(0x4010b0)
执行main结束:
exit(0x4010d0)
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
在节头表中,.got.plt的信息如图:
可以看出.got.plt节从虚拟地址0x404000开始。
在dl_init前:
在dl_init后:
调用前从0x404008开始的16个字节(第一行后一半以及第二行前一半)均 为00,调用后这16个字节
变成了90 21 3f 0d 8a 7f 00 00 b0 bb 3d 0d 8a 7f 00 00
5.8 本章小结
本章对可重定位目标文件链接形成可执行目标文件的过程进行了分析。将可执行文件hello反汇编后与hello.o进行了对比,并用edb对可执行文件hello的虚拟地址空间进行了分析。
第6章 hello进程管理
6.1 进程的概念与作用
进程:一个执行中程序的实例。
进程指程序的一次运行过程,更确切的说,进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因而进程具有动态含义。同一个程序处理不同的数据就是不同的进程。
作用:进程提供给应用程序两个关键抽象
- 逻辑控制流:
每个进程似乎独占地使用CPU;
§ 通过OS内核的上下文切换机制提供。
(2)私有地址空间:
§ 每个进程似乎独占地使用内存系统;
§ OS内核的虚拟内存机制提供。
进程的引入简化了程序员的编程以及语言处理系统的处理,即简化了编程、编译、链接、共享和加载等整个过程。
6.2 简述壳Shell-bash的作用与处理流程
shell 是linux系统中一个交互型应用级程序,代表用户运行其他程序。
处理流程:
- 在shell命令行提示符后输入命令:./hello并enter
- shell命令行解释器构造argv和envp
- 调用fork()函数,创建一个子进程,与父进程shell完全相同(只读/共享),包括只读代码段、可读写数据段、堆以及用户栈等
- 调用execve()函数,在当前进程(新创建的子进程)的上下文中加载并运行hello程序。将hello中的.text节、.data节、.bss节等内容加载到当前进程的虚拟地址空间(仅修改当前进程上下文中关于存储映像的一些数据结构,不从磁盘拷贝代码、数据等内容)
- 调用hello程序的main()函数,hello程序开始在一个进程的上下文中运行。int main(int argc, char *argv[], char *envp[])
6.3 Hello的fork进程创建过程
在终端中输入./hello后,shell会判断出这不是一个系统的内置命令,于是调用fork()函数创建一个子进程。新创建的子进程几乎和父进程相同。子进程拥有与父进程相同的用户级虚拟地址空间(但是是独立的),以及父进程任何打开文件描述符相同的副本。
fork函数的函数原型为int fork(void),调用一次,返回两次。子进程中,fork返回0;父进程中,返回子进程的PID。
6.4 Hello的execve过程
Linux系统中,可通过调用execve()函数来启动加载器。. execve()函数的功能是在当前进程上下文中加载并运行一个新程序。
execve()函数的用法如下:
int execve(char *filename, char *argv[].*envp[);
filename是加载并运行的可执行文件名(如./hello),可带参数列表argv和环境变量列表envp。若错误(如找不到指定文件filename ),则返回-1,并将控制权交给调用程序;若函数执行成功,则不返回,最终将控制权传递到可执行目标中的主函数main。它会覆盖当前进程的代码、数据、栈,保留有相同的PID,继承已打开的文件描述符和信号上下文。
6.5 Hello的进程执行
6.5.3用户态与核心态的切换:
用户态和核心态的区别就是:两者的操作权限不同。内核本质上是一种特殊的软件程序,能够控制计算机的硬件资源,例如协调CPU资源,分配内存资源。
而用户态就是提供应用程序运行的空间,为了使应用程序访问到内核管理的资源,内核必须提供一组通用的访问接口,这些接口就叫系统调用。
从用户态到核心态可以有三种方法:
1、 系统调用:系统调用本身就是中断。
2、异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会 触发切换。
3、外设中断:当外设完成用户的请求时,会向CPU发送中断信号。
6.5.1进程的上下文:
进程的物理实体(代码和数据等)和支持进程运行的环境合称为进程上下文。由进程的程序块、数据块、运行时的堆和用户栈等组成的用户空间信息被称为用户级上下文;由进程标识信息、进程现场信息、进程控制信息和系统内核栈等组成的内核空间信息被称为系统级上下文;处理器中各寄存器的内容被称为寄存器上下文(也称硬件上下文),即进程的现场信息。
6.5.2进程时间片:
进程的运行本质上是CPU不断从程序计数器 PC 指示的地址处取出指令并执行,值的序列叫做逻辑控制流。操作系统会对进程的运行进行调度,执行进程A->上下文切换->执行进程B->上下文切换->执行进程A->如此循环往复。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。在一个程序被调运行开始到被另一个进程打断,中间的时间就是运行的时间片。
6.6 hello的异常与信号处理
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常分为:异常可以分为四类:中断、陷阱、故障、 终止。
可能产生的信号:使用Ctrl+C发出SIGINT信号,使得程序终止;使用Ctrl+Z发出SIGTSTP信号,程序停止直至下一个SIGCONT;使用kill发出SIGTERM,挂起的进程将会被终止。
输入命令:./hello 120L022123 Lu_Ming 1
乱按:多余的内容被加载到缓冲区作为未知命令处理
按下Ctrl -C:此时程序被立即终止
按下Ctrl-Z:程序被挂起
此时使用jobs命令可以查看到被挂起的进程hello,其状态为Stopped
使用ps命令可以查看到进程列表,表中有hello的pid:
这时再使用kill命令,再次查看进程列表,发现hello进程变成了killed状态:
使用pstree命令:
使用fg命令:
6.7本章小结
本章对hello进程的执行过程进行了探究,分析了shell的作用和执行流程,fork创建进程的方式,execve加载进程的方式,了解到了hello进程执行过程中会遇到的异常和信号以及它们的处理过程。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1)逻辑地址:程序经过编译后出现在汇编代码中的地址,用来指定一个操作数或者是一条指令的地址。
2)线性地址:是逻辑地址到物理地址变换之间的中间层。
3)虚拟地址:虚拟内存中的地址。
4)物理地址:物理内存中的地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。全局的段描述符,放在全局段描述符表中,一些局部的段描述符,放在局部段描述符表(LDT)中。 给定一个完整的逻辑地址段选择符和段内偏移地址,确定要转换的是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟地址包含两个部分:VPN(虚拟页号)、VPO(虚拟页面偏移)
物理地址包含两个部分:PPN(物理页号)、PPO(物理页面偏移)
其中虚拟页面偏移和物理页面偏移是相同的。
页表基址寄存器指向当前的页表。MMU通过VPN在页表中找到对应的页表 项PTE。将PTE中的PPN与VPO串联,得到最终的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
CPU生成一个VA。MMU用VA中的VPN向TLB请求对应的PTE。如果命中,则直接与VPO并联获得PA。否则MMU生成PTE地址,并依次向高速缓存和主存请求得到PTE,如果不命中则逐级向下请求。如果最终发现虚拟页是未缓存的,会导致缺页故障。则将磁盘的虚拟页加载到内存中,然后再执行导致缺页的指令。
7.5 三级Cache支持下的物理内存访问
得到物理地址之后,先通过组索引在L1cache中寻找对应组。如果存在,则比较标志位,相等后检查有效位是否为1。如果有效则命中。否则按顺序用相同的方法对L2cache,L3cache和内存进行相同操作,直到出现命中。然后逐级将数据返回,如果有空闲块则将目标块放置到空闲块中,否则使用替换策略将缓存中的某个块替换为当前的新数据。
7.6 hello进程fork时的内存映射
虚拟内存和内存映射解释了fork函数如何为每个进程提供虚拟地址空间:
为新进程创建虚拟内存:
- 创建当前进程的的mm_struct, vm_area_struct和页表的原样副本.。
- 两个进程中的每个页面都标记为只读
- 两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制
在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存。随后的写操作通过写时复制机制创建新页面
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行新程序hello的步骤:
- 删除已存在的用户区域
- 创建新的区域结构:
1.代码和初始化数据映射到.text和.data区(目标文件提供)
§ 2..bss和栈映射到匿名文件
(3)设置PC,指向代码区域的入口点:
§ Linux根据需要换入代码和数据页面
7.8 缺页故障与缺页中断处理
缺页故障:虚拟内存中的字不在物理内存中 (DRAM 缓存不命中)
缺页中断处理:
1) 处理器将虚拟地址发送给 MMU
2) MMU 使用内存中的页表生成PTE地址
3) 有效位为零, 因此 MMU 触发缺页异常
4) 缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘)
5) 缺页处理程序调入新的页面,并更新内存中的PTE
6) 缺页处理程序返回到原来进程,再次执行导致缺页的指令
7.9动态存储分配管理
在程序运行时程序员使用动态内存分配器 (比如malloc) 获得虚拟内存。动态内存分配器维护着一个进程的虚拟内存区域,称为堆.。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。
分配器的类型:
§ 显式分配器: 要求应用显式地释放任何已分配的块。
§ 隐式分配器: 应用检测到已分配块不再被程序所使用,就释放这个块。
策略:
方法1: 隐式空闲链表
¢ 对于每个块都需要知道块的大小和分配状态, 如果块是对齐的,那么一些地址低位总是0,使用0位作为一个已分配/未分配的标志,读块大小字段时,必须将其屏蔽掉。
方法2:显式空闲链表
¢ 只记录空闲块链表, 而不是所有块, “下一个” 空闲块可以在任何地方,因此需要存储前驱/后继指针,而不仅仅是大小,还需要合并边界标记。
7.10本章小结
本章介绍了hello的存储器地址空间,段式管理,页式管理,TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,hello进程fork时的内存映射,hello进程execve时的内存映射,缺页故障与缺页中断处理,动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m字节的序列:B0 , B1 , .... , Bk, .... , Bm-1 ,所有的I/O设备都被模型化为文件: /dev/sda2(用户磁盘分区) /dev/tty2(终端)
I/O操作可看作对相应文件的读或写
设备的模型化:文件
设备管理:unix io接口
Linux内核给出的一个简单、低级的应用接口,能够以统一且一致的方式执行 I/O操作,包括:
打开和关闭文件:open()和close()
读写文件:read()和write()
改变当前的文件位置:指示文件要读写位置的偏移量和lseek()
8.2 简述Unix IO接口及其函数
接口:
打开文件:通过内核打开文件,内核返回非负整数,成为描述符。描述符表示这个文件。内核记录有关文件的所有信息。
文件位置:每个打开的文件,内核保持一个文件位置k,表示从文件开头起始的字节偏移量。
读写文件:进行复制操作并改变文件位置k的值。
关闭文件:内核释放相应数据结构,将描述符恢复到可用的描述符池中。
函数:
1.int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个文件或创建一个新文件。参数 filename 指向欲打开的文件路径字符串。参数flags为使用的旗标。参数mode则有数种组合, 只有在建立新文件时才会生效。
2.int close(int fd)
进程通过调用close函数关闭一个打开的文件。当使用完文件后若已不再需要则可使用 close()关闭该文件。close()会让数据写回磁盘, 并释放该文件所占用的资源。参数fd 为先前由open()或creat()所返回的文件描述词。
3.size_t read(int fd,void *buf,size_t n)
read()会把参数fd 所指的文件传送n个字节到buf 指针所指的内存中。若参数n为0,则read()不会有作用并返回0。返回值为实际读取到的字节数, 如果返回0, 表示已到达文件尾或是无可读取的数据,此外文件读写位置会随读取到的字节移动。
4.ssize_t wirte(int fd,const void *buf,size_t n)
write()会把参数buf 所指的内存写入n个字节到参数fd 所指的文件内。 当然, 文件读写位置也会随之移动。如果顺利write()会返回实际写入的字节数. 当有错误发生时则返回-1, 错误代码存入errno 中。
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;
}
下面是 vsprintf() 函数的声明:
int vsprintf(char *str, const char *format, va_list arg)
str 是指向一个字符数组的指针,该数组存储了 C 字符串。format是字符串,包含了要被写入到字符串 str 的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。format 标签属性是 %[flags][width][.precision][length]specifier。arg 是一个表示可变参数列表的对象。这应被 <stdarg> 中定义的 va_start 宏初始化。如果成功,则返回写入的字符总数,否则返回一个负数。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
C 库函数 int getchar(void) 从标准输入 stdin 获取一个字符(一个无符号字符)。这等同于 getc 带有 stdin 作为参数。
下面是 getchar() 函数的声明:int getchar(void)
返回值:该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法,Unix IO接口及其函数,printf函数和getchar函数的实现。
结论
- 初始时,用文本编辑器编写hello.c并存储在磁盘上。
- cpp预处理hello.c生成文本文件hello.i。
- 编译器cc1编译hello.i生成汇编语言文件hello.s。
- 汇编器as将hello.s翻译成机器语言指令,生成可重定位目标文件hello.o。
- 链接器ld将hello.o和其它需要的可重定位目标文件链接,生成可执行目标文件hello。
- 使用shell执行hello。
- shell使用fork创建子进程,使用execve加载并运行hello。hello运行过程中可能遇到异常,收到信号。此时调用异常处理程序。
- hello调用printf,使用UNIX I/O来进行输出。
- hello运行结束,被shell回收。
附件:
hello.c :源程序
hello.i :经过预处理的代码(源程序)
hello.s :汇编语言程序
hello.o :可重定位目标文件
hello :可执行目标文件
hello_obj.txt :hello的反汇编代码
hello_objdump.txt :hello.o的反汇编代码
hello_elf.elf : hello的elf文件
hello.elf :hello.o的elf文件
参考文献
[1] Randal E. Bryant David R. O’Hallaron 深入理解计算机系统 机械工业出版社,原书第3版
[2] C 语言教程 | 菜鸟教程
[3] Linux 教程 | 菜鸟教程