计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能
学 号 2022113221
班 级 2203601
学 生 陈益可
指 导 教 师 吴锐
计算机科学与技术学院
2024年5月
摘 要
Hello程序是大部分程序员生涯中所写的第一个程序。本文以一个简单的hello.c程序开始,介绍了一个程序在Linux下运行的完整生命周期。主要内容包括hello是如何一步步通过预处理、编译、汇编、链接,以及操作系统和硬件如何管理该程序的运行。
关键词:操作系统;编译;汇编;链接;进程;存储;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
第1章 概述
1.1 Hello简介
P2P指的是Program to Progress 。Hello程序要生成可执行文件首先要进行预处理:预处理器根据以#开头的命令,修改初原始的C程序,生成hello.i文件;然后进行编译,将文本文件hello.i翻译成汇编文件hello.s;接着汇编,将hello.s翻译成机器语言指令并生成可重定位文件hello.o。最后进行链接,由链接器生成可执行文件。
020: from Zero-0 to Zero-0 ,意为从零到零的过程。程序并不是一开始就在内存空间中的,也就是一开始为0。OS为hello fork一个子进程,然后在execve执行hello程序,OS会为他开辟一个块虚拟内存,并将程序加载到虚拟内存映射到的物理内存中。当程序执行完,OS回收这一程序,同时为该程序的开辟的内存空间也会被回收,此时又变为0。
1.2 环境与工具
硬件环境:CPU: 12th Gen Intel(R) Core(TM) i7-12700H 2.30 GHz 64位操作系统
软件环境:Windows11, Ubuntu 18.04 LTS, Oracle VM VirtualBox
开发工具:Visual Studio Code , gcc
1.3 中间结果
hello.i: 预处理后C程序。
hello.s:汇编程序
hello.o:可重定位目标文件
hello.elf:hello.o的elf文件
hello:可执行目标文件
1.4 本章小结
对于P2P和020的具体含义的理解,对报告的内容有概括性的了解,对全部计算机系统课程有一个整体的认识。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理一般指由预处理器对程序源代码进行处理的过程。预处理器(cpp)根据字符#开头的命令,修改原始的C程序。
预处理器可以在编译器对C程序编译之前,进行重新编辑(修改源代码)工作。这个过程包括:
1.将使用#define指令进行的宏定义进行匹配替换
2.将使用#include指令包含的头文件添加到当前文件中
3. 将预定义指令删除
4. 删除注释
5. 可以使用gcc –E Hello.c 查看预编译器的输出结果,例如下面程序的输出结果将包含stdio.h到当前文件,
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
图 1 预处理输入命令图
2.3 Hello的预处理结果解析
图 2 预处理部分结果图
上图可见.c源程序中带#include的程序被预处理为三千行左右的 #语句。代表着#include<stdio.h> 、#include<unistd.h>、 #include<stdlib.h>语句被替换展开,头文件信息引用到.i文件中。
图 3 预处理main函数结果图
可知预处理过程中,main函数没有太多变化。
2.4 本章小结
本章介绍了预处理的概念和作用,然后介绍了linux系统下的生成hello.i的预处理命令,最后通过查看hello.i文件中的内容来了解预处理的实际结果。得知预处理会读取头文件的内容并把它直接引入到程序文本中供主函数调用。
第3章 编译
3.1 编译的概念与作用
概念:编译是指将预处理程序转换为计算机可以执行的机器语言代码(汇编程序)的过程,编译将与预处理文件中的高级语言以文本格式转换为汇编语言,得到一个用汇编语言描述程序的文本文件。
作用:将程序员编写的高级语言代码转换为计算机可以理解的机器语言代码,以便计算机执行程序。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图 4 编译输入命令图
3.3 Hello的编译结果解析
3.3.1 数据
1、字符常量位于静态存储区
图 5 汇编语言字符常量
2、局部变量
对程序中局部变量i,储存在栈中,因为类型为int,大小4字节,为-4(%rbp)。
图 6 汇编语言中局部变量
3、函数参数
实际为局部变量,函数调用开始时储存在栈中。
图7 源程序中函数参数
图 8 汇编语言中函数参数
4、立即数
直接在汇编代码中
图 9、10、11 汇编代码中立即数
5、数组被连续的存储在栈中,argv存储在-32(%rbp)的位置,且为指针数组(数组元素占8个字节)。所以访问数组元素要在-32(%rbp)后加8的整数。
图 12 汇编语言中数组元素
3.3.2 操作
1、控制转移if(argc!=5)
图 13 对应汇编代码
源程序中涉及了if语句和argc!=4表达式 ,汇编文本中,通过cmpl比较,然后用je指令判断是否进行相应的跳转,这里je是相等就跳转,与原始的!=正好相反,所以是跳转到对立的分支部分,即L2。
2、for(i=0;i<10;i++)
图 14 对应汇编代码
源程序中涉及了for语句和i<10表达式,汇编指令中,在执行完一次操作后,对参数i加1.,然后通过cmpl指令判断循环的边界条件,根据条件码的结果跳转到指定部分。不过这里有一点差异,原始是判断i<10而这里用的是等价的i<=9操作。
3、i++
图 15 对应汇编代码
运算操作,同上。
4、赋值操作
将0赋予局部变量。
图 16 对应汇编代码
5、 函数调用
汇编代码中,call指令实现了对exit、printf、atoi和sleep函数的调用
图 17 对应汇编代码
6、类型转换
上图atoi函数为类型转换的函数,将字符串转换成int类型的数据。
3.4 本章小结
本章介绍了编译的概念和作用,了解了编译的指令,并以hello为例重点分析了编译后的hello.s文件,通过源程序,分析了各种数据和各种操作在汇编代码中的实现。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编指的是汇编器将.s 文件翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在.o后缀的文件中的过程。
作用:汇编将前一步生成的汇编代码真正转化为了机器可读的机器代码,汇编生成的可重定位目标文件是二进制文件,而不再是文本文件了。其代码和数据可以和其他可重定位目标文件合并,提高了运行效率。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
图 18 汇编输入命令图
4.3 可重定位目标elf格式
1、获得hello.o的ELF格式的命令:readelf -a hello.o > hello.elf
2、ELF文件分析
(1)ELF头:ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等。
图19 ELF头
(2)节头表:记录每个节的名称, 类型, 属性(读写权限), 在ELF文件中所占的长度对齐方式和偏移量。
图20 节头表
(3)重定位节:重定位条目告诉链接器在目标文件合并成可执行文件时如何修改这个引用。offset是需要被修改的引用的节的偏移, 符号标识被修改引用应该指向的符号. type告知连接器如何修改新的引用, 加数是一些类型的重定位要用它对被修改引用的值做的偏移调整量。
图21 重定位节
(4)符号表:它存放程序中定义和引用的函数和全局变量的信息, 符号表不包括局部变量的条目。其中Num位符号的编号,Bind为符号类型,Name为符号的名字。
图22 符号表
(5)ELF文件格式以下部分组成:
.text:程序的机器代码
.rodata:只读数据,例如跳转表
.data:已初始化的全局和静态C变量
.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量
.symtab:一个符号表,存放一些在程序中定义和引用的函数和全局变量的信息
.rel.text:.text节中需要被修正的信息
.rel.data:被模块引用或定义的所有全局变量的重定位信息
.debug:调试符号表
.line:原始C源程序中的行号和.text节中机器指令之间的映射
.strtab:一个字符串表(包括.symtab和.debug节中的符号表)
4.4 Hello.o的结果解析
1、objdump -d -r hello.o 得到如下文本内容,
图23 hello.o反汇编部分结果
2、对比分析
(1)反汇编文件相比汇编文件多了指令对应的机器指令以16进制显示在左侧,指令包括了指令本身的编码和操作所需的立即数等。
(2)分支转移:在hello.s中分支跳转都是直接将跳转目标记为一个标号,形如.L2、.L3,而在反汇编中跳转的目标为一个函数名加立即数。
图24 分支转移的区别(上为反汇编程序)
(3)函数调用
在hello.s中函数调用时直接在call指令加函数名,而在反汇编程序调用写入的是一个具体的地址,因为是外部的函数还无法确定地址,所以初始的都设置为e800000000,对应的语句下方还有条目,便于重定位处理。
图25 函数调用(上为反汇编程序)
综上,两者并无太大差别,指令部分顺序和规则基本一致,除了语句前多了若干16进制数和在分支控制和函数调用上有着一些差异。
4.5 本章小结
本章介绍了汇编的概念和作用,了解汇编的命令,实际分析ELF格式文件的内容了解了可重定位目标文件的具体内容形式,最后对照分析原始的汇编代码hello.s和反汇编程序。
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可以被加载到内存执行。链接可以执行于编译时,甚至运行时。
作用:链接使分离编译成为可能,链接器将程序中的多个模块组合在一起,生成可执行文件或共享库。它确保程序的各个部分正确连接,并最终生成可供执行的文件。
注意:这儿的链接是指从 hello.o 到hello生成过程。
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
图26 链接命令
5.3 可执行目标文件hello的格式
在终端输入readelf -a hello ,可知文本信息在终端。
分析:
1、ELF头:与前者ELF头没有太大区别,包含了各个节的类型、位置、所占空间大小等信息。
图27 ELF头
2、节头表:节头部表对hello中所有信息进行了声明,包括了大小、偏移量、起始地址以及数据对齐方式等信息。根据这些信息,可以计算每一个节所在的位置。
图28 部分节头表
3、程序头:标记了程序了类型、偏移量、虚拟地址和物理地址。
图29 程序头
4、重定位头:包含了链接过程中的重定位信息。
图30 部分重定位头
5、符号表:由于我们在链接时链接了共享库(-dynamic-linker/lib64/ld-linux-x86-64.so.2) ,所以符号表中包含.dynsym(动态符号表),其余符号记录在.symtab节中。
图31 部分符号表
5.4 hello的虚拟地址空间
使用edb加载hello,通过Data Dump查看本进程的虚拟地址空间各段信息。
图 32 hello的虚拟地址空间
对照5.3,可以根据其中每一节对应的起始地址在edb中找到响应信息,比如:
由图28可知,.interp段地址为400200
5.5 链接的重定位过程分析
输入命令objdump -d -r hello,得到hello反汇编结果
图33 hello反汇编部分结果
不同:
1、库函数
库函数的加入是程序链接到库文件的结果。
图34 库函数
2、正确的地址
因为在连接过程中,经过了符号解析和重定位的过程,将重定位条目中的类型、修正值等参数计算出正确的结果后、填充到原本未确定的部分,从而完善了指令。所以.o.文件未确定的地址,在反汇编文件下替换成实际地址。
图35 分支跳转中的正确地址
图36 函数调用中的正确地址
5.6 hello的执行流程
使用edb的symbol viewers工具调试查询跳转的地址:
1、_dl_start、_dl_init
2、_start、dl_main
3、_main、_printf、_atoiget、_sleep、_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x
4、Exit
对应地址:
0x00007fe06be75ea0: ld-2.27.so!_dl_start
0x00007fe06be847d0: ld-2.27.so!_dl_init
0x0000000000400550: hello!_start
0x00007fe0459bb660: ld-2.27.so!dl_main
0x0000000000400582: hello!main
0x0000000000400500: hello!printf@plt
0x0000000000400520: hello!atoi@plt
0x0000000000400540: hello!sleep@plt
0x0000000000400510: hello!getchar@plt
0x0000000000400530: hello!exit@plt
0x00007fe0459d6210: ld-2.27.so!_exit
5.7 Hello的动态链接分析
动态链接机制是通过过程链接表(PLT)和全局偏移量表(GOT)实现的。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
从symbol viewer获取两者头地址,如下
运行前:
图 37 运行前.got和.got.plt信息
运行后:
图 38 运行后.got和.got.plt信息
图37 头地址
分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章主要介绍了连接的概念与作用,并对链接生成的ELF文件格式和连接过程进行了分析,从虚拟空间,重定位的角度,展现了链接在运行程序过程中的重要性。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
1、概念:进程是指一个执行的程序得实例。或者说是一个拥有某个功能的数据集合的活动。它是操作系统中最基本的执行单元与操作单元。
2、作用:进程的概念为我们提供了两个关键的抽象:(1)一个独立的逻辑控制流:他提供一个假象,好像我们的程序在独占地使用处理器(2)一个私有的地址空间:他提供一个假象,好像我们的程序在独占地使用内存系统。
作用:Shell是一个交互型应用级程序,为使用者提供操作界面,接收用户命令,然后调用相应的应用程序。实质上是一个命令语言解释器。通过它我们可以实现与操作系统的交互。
处理流程:
1、读取用户输入的指令。
2、分析指令,识别指令的名称及其附带参数。
3、搜索指令的存放位置,确定要运行的指令文件。
4、利用fork()函数生成一个子进程,以执行该指令。
5、在子进程中,载入指令文件,并根据参数执行对应的操作。
6、指令执行完毕后,将结果传递给父进程。
7、父进程在命令行界面上展示结果,并回收已完成的子进程资源。
父进程通过调用fork函数创建一个新的运行的子进程。子进程会获得与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆共享库以及用户栈。在父进程创建新的子进程时,子进程还会获得与父进程任何打开文件描述符相同的副本。父进程和子进程之间的最大区别在于它们有不同的PID,但属于同一进程组。
6.4 Hello的execve过程
子进程创建出来后会去调用execve()函数来执行子程序。首先会删除已存在的用户区域,为hello的代码、数据、.bss和栈区域这些私有的内容创建新的区域,之后在进行动态链接到共享区域中。最后exceve()函数设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点进行hello的执行。
6.5 Hello的进程执行
1、上下文信息
上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
2、进程时间片
时间片是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间。
3、进程调度
因为一般情况下进程数多于处理机数,所以在进程执行的某些时刻,内核可以决定抢占当前进程,并重新打开一个先前被抢占的进程。这种决策就叫做调度。
4、用户态与核心态转换
处理器提供一种限制一个应用可以执行的指令以及它可以访问的地址空间范围的机制,并通过设置一种模式位来提供这种功能。设置了模式位,进程就处于内核模式,并允许执行特权指令。反之,即用户模式。
6.6 hello的异常与信号处理
图38 异常类型
四种异常处理流程:
图39 信号类型
运行测试
图40 正常运行结果
乱按:输出栏多出输入内容,程序依然正常运行
图41 乱按结果
Ctrl-Z: 进程收到SIGTSTP信号后停止hello进程将其暂时挂起。
图42 Ctrl-Z结果
Ctrl-C:按下Ctrl-C后进程收到SIGINT信号,shell终止并回收掉hello进程。
图43 Ctrl-C结果
Ctrl-Z后各个命令:
1、ps:hello进程仍然存在。
图44 ps命令结果
2、jobs:hello作业编号是1,目前被挂起。
图45 jobs命令结果
3、pstree:所有进程信息
图46 pstree命令部分结果
4、fg:继续执行之前的进程
图47 fg命令部分结果
5、kill:杀死进程,hello进程由ps指令可知pid为3102,故输入指令kill -9 3102
图47 kill命令结果与验证
6.7本章小结
本章介绍了进程的概念和作用,还有异常的类型和相对应的不同的处理方式。通过依次介绍程序加载和执行的过程,展现了程序通过shell执行的过程逻辑,我们以各种方式实际运行hello程序,展现了shell对于各种异常和信号的处理情况并通过简单的分析介绍了产生结果的原因。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:在hello程序的编译过程中,源代码中的变量和函数引用会被编译器转换为逻辑地址。这些地址通常是由段基值和偏移量组成的,段基值确定变量或函数在整个存储空间中的位置,而偏移量确定其在段内的位置。
线性地址:在hello程序的执行过程中,逻辑地址会首先被转换为线性地址。线性地址是一个32位无符号整数,可以用来表示高达4GB的地址空间。
虚拟地址:在Windows操作系统中,每个进程都有自己独立的4GB虚拟地址空间。这4GB地址空间中的部分区域被映射到物理内存,部分区域映射到硬盘上的交换文件,还有部分区域没有映射任何内容。hello程序在运行时,其使用的都是虚拟地址空间中的地址。当程序试图访问某个虚拟地址时,内存管理单元(MMU)会检查该地址是否有效,并自动将其转换为物理地址。
物理地址:物理地址是在存储器里以字节为单位存储信息的地址,每个字节单元都有一个唯一的物理地址。在hello程序执行时,CPU和内存之间的数据交换都是基于物理地址进行的。当CPU需要从内存中读取数据或写入数据时,它会使用物理地址来指定数据的位置。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是一种应用于IA32架构的管理方式。一组寄存器存储着当前进程各个分段(如代码分段、数据分段、堆栈分段)在描述符表中的标识符,这些标识符可以用于查询每个分段的逻辑地址。当获得了逻辑地址时,可以通过简单的计算来得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
36的VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。最后得到PPN,PPO与VPO相同,得到PA。
图 48 四级页表图
7.5 三级Cache支持下的物理内存访问
物理地址分为标记,组索引和块偏移。首先,在L1 Cache中匹配组索引位,若匹配成功,则根据标记和偏移的匹配结果决定缺失或是命中。若组索引匹配不成功,则进入下一级Cache,重复直至进入内存。
7.6 hello进程fork时的内存映射
在用户输入运行指令后,如果shell识别出该指令并非内置命令,便会将其视作一个可执行程序,并为其fork出一个子进程。在这个过程中,内核会为这个新进程建立必要的数据结构,并赋予它一个独一无二的PID)。为了构建这个新进程的虚拟内存空间,内核会复制原进程的mm_struct、区域结构和页表。当fork操作在新进程中完成后,其虚拟内存与原进程在fork调用时的虚拟内存状态完全一致。若这两个进程中的任何一个尝试进行写操作,写时复制机制会生成新的页面,从而确保每个进程都拥有独立的地址空间。
7.7 hello进程execve时的内存映射
与6.4内容大致相同。执行execve()系统调用时,当前进程的地址空间会加载一个新程序,该程序会替换现有的代码段、数据段和堆栈。操作系统会参考新程序的ELF文件格式,重新构建进程的内存映射,这包括载入新程序的各个段(如代码段、数据段等)以及映射所需的共享库。同时,系统会关闭原有的文件描述符,并打开新程序运行所需的文件描述符。
图 49 加载器时如何映射用户地址空间的区域的
7.8 缺页故障与缺页中断处理
1、若PTE 中的有效位是零,所以 MMU 触发了一次异常,传递 CPU 中的控制到操作系统内核中的缺页异常处理程序。
2、缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。
3、缺页处理程序页面调入新的页面,并更新内存中的 PTE。
4、缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU 将引起缺页的虚拟地址重 新发送给 MMU
图 50 linux缺页处理
7.9动态存储分配管理
动态内存管理涉及在运行时动态地分配和释放内存。在 C 语言中,这主要通过 malloc(或相关函数如 calloc 和 realloc)以及 free 函数来实现。动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为一组不同大小的块的集合来维护。分为显式分配器、隐式分配器。显式分配器:要求应用显式地释放任何已分配的块。例如,c标准库提供一种叫做malloc程序包的显式分配器。c程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。
隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。
7.10本章小结
Linux为每个进程提供统一独享的虚拟地址空间,系统运用段式管理将进程的信息映射到虚拟空间上,又用页式管理虚拟内存地址被翻译为物理地址、虚拟内存映射到物理内存上。然后内存映射的角度,解读了Fork、execve函数在虚拟内存层面的作用。了解了计算机的缺页处理和动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux的设备管理的主要任务是控制设备完成输入输出操作,所以又称输入输出(I/O)子系统。
它的任务是把各种设备硬件的复杂物理特性的细节屏蔽起来,提供一个对各种不同设备使用统一方式进行操作的接口。
Linux把设备看作是特殊的文件,系统通过处理文件的接口—虚拟文件系统VFS来管理和控制各种设备。
8.2 简述Unix IO接口及其函数
Linux/unixI/O:将设备映射为文件的方式,允许Unix内核引出一个简单、低级的应用接口。 Linux/unix IO的系统调用函数很简单,它只有5个函数:open (打开)、close (关闭)、read (读)、write (写)、lseek (定位)。
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;
}
可以看到printf将fmt和后续参数arg、buf部传入了vsprintf函数,然后调用了write函数,并获取了它的返回值,即输出长度作为返回值。
vsprintf的代码如下:
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的作用就是格式化。它接受确定输出格式的格式字符串fmt,产生格式化输出,同时返回要打印出来的字符串的长度。而另一个函数write是一个系统函数,功能是将buf中的i个char型元素写入终端。
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数显示格式化了的字符串。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar() 是C语言标准库中的一个函数,用于从标准输入流(stdin)中读取一个字符。
int getchar(void) {
unsigned char c;
if (read(0, &c, 1) != 1) {
return EOF; // 读取失败,返回 EOF 表示文件结束或出错
}
return (int)c; // 返回读取的字符
}
这个实现基于底层的系统调用 `read()`,它从文件描述符(0 表示标准输入)中读取一个字节,并将其存储在 `c` 变量中。如果读取成功,`read()` 返回 1,否则返回 -1。如果读取失败,`getchar()` 返回 EOF(End of File)来表示文件结束或出错。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux系统中IO设备管理方法,简述了IO接口及其函数并对printf和getchar函数的实现进行了简单的分析,两者体现了LinuxIO与标准IO的细微差别。
(第8章1分)
首先,我们在计算机上利用文本编辑器,采用C语言编写了名为hello.c的源文件。接着,这个文件会经过预处理阶段,将相关的头文件内容融合进来,生成了hello.i文件。然后,编译器对hello.i进行编译,生成了汇编文件hello.s。但计算机并不能直接理解和执行汇编代码,因此需要汇编器进一步处理,将汇编语言转换为机器语言,并生成了可重定位的目标文件hello.o,这是一个二进制文件。
然而,尽管hello.o已经是机器语言文件,但其中的指令还未被分配具体的虚拟地址。这时,链接器会介入,对hello.o中调用函数的指令进行地址重定位,并将所需的系统函数(如printf.o等)链接到hello.o,最终生成了可执行的目标文件hello。
hello文件已经可以在计算机上运行了。当我们在shell中输入命令./hello来运行这个程序时,操作系统会为hello创建一个独立的子进程,确保程序在独立的运行环境中执行。
程序运行过程中,涉及到对存储和地址的操作。值得注意的是,hello程序中的地址是虚拟地址,需要转换为物理地址才能进行实际操作。此外,为了让hello程序能够正常运行,用户需要输入学号、姓名以及间隔时间,这也让我们更加关注计算机是如何管理输入/输出设备的。
最后,当hello程序运行完毕,shell作为父进程会负责回收并终止这个已经结束的子进程。
简单的一个hello程序,从它一步一步的变化,到一瞬的输出,,似乎不起眼。计算机做的这一份报告的事情也是就在此瞬完成。我们把《深入理解计算机系统》学完花了12个周的时间,所学的一切,也在此瞬。科学世界有多少瞬间,化学分子的碰撞,物理学电子的弹射,他们离我们似乎很近,又无时无刻在发生。而计算机不也是吗,在我即将完成这个报告的时候,回望他的从0到1,又有多少个像hello一样的进程如同那一瞬的匆匆过客。Hello不只是俗套的例子,更是浓缩了计算机系统这门艺术的质朴浪漫。
(结论0分,缺失 -1分,根据内容酌情加分)
hello.c:C语言源程序。
hello.i: 预处理后C程序。
hello.s:汇编程序
hello.o:可重定位目标文件
hello.elf:hello.o的elf文件
hello:可执行目标文件
(附件0分,缺失 -1分)
为完成本次大作业你翻阅的书籍与网站等
[1] 《深入理解计算机系统》 Randal E.Bryant David R.O’Hallaron 机械工业出版
社
[2] Linux下 可视化 反汇编工具 EDB 基本操作知识 https://blog.csdn.net/hahalidaxin/article/details/84442132
[3] 彻底搞懂程序链接过程之动态链接 https://blog.csdn.net/daocaokafei/article/details/118614187
[4] printf 函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)