Hello World程序是大部分程序员生涯中所写的第一个程序。在这个简单的程序背后,是系统与硬件通过庞大的操作将这样一个基础的程序实现。本文以C语言程序hello.c为研究对象,对其在linux系统下的一整个生命周期进行探究,从编译、链接、加载、运行,一直到终止、回收。
关键词:计算机系统;程序生命周期;linux
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
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 -
8.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
1.1 Hello简介
P2P:P2P即Program To Process,指hello.c文件从可执行程序(Program)变为进程(Process)的过程。在linux系统下,hello.c依次经过预处理、编译、汇编、链接最终成为可执行程序hello。在shell中输入运行命令后,shell为其fork一个子进程,hello便成为了一个进程。
020:020即“From 0 To 0”。linux调用execve,映射虚拟内存,通过mmap为其申请一片空间。进入程序入口后开始载入物理内存,然后执行目标代码。程序运行结束时,hello进程被shell父进程回收,释放内存并且删除有关的上下文。至此,hello回到了“0”。
1.2 环境与工具
硬件环境:Intel(R) Core(TM) i7-10875H CPU @2.30GHz;16GB RAM;1024GB SSD
软件环境:Windows10 64位;VirtualBox;Ubuntu 20.04 LTS 64位
开发与调试工具:Visual Studio 2022 64-bit;CodeBlocks 64-bit;
gcc;GNU gdb;edb;gedit;notepad++
1.3 中间结果
作用 | |
hello.c | C语言源程序,文本文件 |
hello.i | 经预处理的源程序,文本文件 |
hello.s | 经编译的汇编文件,文本文件 |
hello.o | 经汇编的可重定位目标文件,二进制文件 |
hello.elf | 经readelf得到的elf格式信息,文本文件 |
hello.asm | 经objdump得到的反汇编文件,文本文件 |
hello | 经链接得到的可执行目标文件,二进制文件 |
1.4 本章小结
Hello World程序是大部分程序员生涯中所写的第一个程序,经典C语言教材The C Programming Language使用它作为第一个演示程序,因而闻名世界。在本文章中,我们将研究这样一个简单程序的一生,从头至尾了解Hello World的执行细节。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理的概念
预处理(preprocessing)指的是预处理器(cpp)根据以字符“#”开头的命令,修改原始的C程序并生成.i文件的过程。例如,“#define”就会预先用实际值替换其定义的字符串,“#include”则是预先读取头文件并插入程序文本之中。此外,预处理还会删除程序中的注释与多余的空白字符。
2.1.2预处理的作用
预处理能够使程序简洁易读、便于维护,帮助程序员节省工作量。预处理的过程中并未直接解析程序的代码,而是对程序文本进行了处理。简单来说,只是处于文本层面上的一种操作。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
hello.c源程序仅有23行,但经预处理后生成的hello.i有3060行。打开.i文件可以发现,源程序中的注释被删除,“#include”引用的头文件也依次展开,而原本的代码则被放在了.i文件的最后。
2.4 本章小结
本章介绍了预处理的概念与作用,并实际地结合了Unbuntu系统下hello.c文件进行预处理之后得到的hello.i文件对预处理进行了解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念
编译是指编译器(cc1)将预处理过的文件转换为汇编文件,即将预处理所得的.i文件翻译成.s文件。
3.1.2编译的作用
汇编语言将不同种类的语言翻译成相同的形式,更便于汇编器将其转为机器码供机器执行。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.0汇编指令
对hello.s文件汇编指令的解析:
内容 | 含义 |
.file | 声明源文件 |
.text .section .rodata .align | 代码节 只读数据段 数据/指令地址的对齐方式 |
.global | 全局变量 |
.type | 表示是函数类型/对象类型 |
.data | 存放已经初始化的全局和静态C 变量 |
.size | 声明大小 |
.long .string | 声明long类型/string类型 |
3.3.1数据
3.3.1.1字符串
程序中有两个字符串,且都在只读数据段中。
如下图所示,这两个字符串均作为printf的参数。
3.3.1.2整型数
在hello.s中共有两个整型数,分别为:
1.局部变量i
局部变量一般存储在寄存器或者栈中。本程序中main函数声明了一个局部变量i,如图存储在栈上-4(%rbp)的位置上。
2.参数argc
argc是用户传给main函数的参数,也保存在栈中。
3.3.1.3数组
程序中的数组为main函数的第二个参数,其起始地址存放在栈中-32(%rbp)的位置上。Printf对其进行了两次访问,寻找参数。
3.3.1.4立即数
立即数直接体现在汇编代码中。
3.3.2操作
3.3.2.1赋值操作
程序中对局部变量i执行了i=0的赋值操作,通过movl指令实现。
3.3.2.2算术操作
程序中i++属于算术操作,在汇编代码中通过addl指令实现。
3.3.2.3关系操作
程序中共有两个关系操作:
1.argc!=4
汇编代码中通过cmpl与je实现,根据比较结果决定是否需要跳转。
2.i<8
同样使用cmpl指令配合另一跳转指令jle。
3.3.2.4数组操作
程序中存在数组char *argv[],通过寄存器寻址对其进行访问,如3.3.1.3所述。
3.3.2.5控制转移
程序中有两处控制转移,均与关系操作相联系,如3.3.2.3所述。
3.3.2.6函数操作
程序中设计的函数操作共有:main,printf,exit,atoi,sleep,getchar。
1.main
函数有两个参数,int argc,char *argv[],它们分别存储在寄存器%edi和%rsi中。main函数返回值为0,即将%eax赋值为0。
2.printf
第一处:
调用printf函数之前,先将参数放入寄存器%rdi中进行传参。由于只有一个参数,编译器将printf函数翻译为puts函数来进行输出。
第二处:
调用printf函数之前,先将三个参数按顺序依次分别存放在寄存器%rdi,%rsi,%rdx中。
3.exit
调用exit函数之前,将立即数1存入%rdi作为参数。
4.atoi
将argv[3]作为参数存入%rdx中,并将返回值存入%eax。
5.sleep
将函数atoi的返回值从%eax传入%edi,作为sleep函数的参数。
6.getchar
此函数没有参数,因此在汇编代码中直接进行调用。
3.3.2.7类型转换
hello.c中的atoi(argv[3])实现了将字符串类型转换为整数类型的类型转换。
3.4 本章小结
本章介绍了编译的概念与作用,编译是将文本文件翻译成汇编文件,为后续转化为二进制机器码做准备的过程。本章实际地结合了Unbuntu系统下hello.s文件,介绍解释了C语言的各类数据与操作如何在汇编代码下实现。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
4.1.1汇编的概念
汇编是指编译器(assembler)将.s汇编文件翻译成机器语言指令,然后把这些指令打包成可重定位目标程序的格式,并将结果输出为.o文件的过程。.o文件是一个二进制文件,它包含程序的指令编码。
4.1.2汇编的作用
将汇编语言翻译成机器语言,使其在链接后能被机器识别并执行。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
首先通过readelf -a hello.o > hello.elf指令得到hello.o文件的elf格式。
4.3.1ELF头
以一个16字节的Magic序列开始,描述生成该文件的系统的字的大小和字节顺序、帮助链接器语法分析和解释目标文件的信息。由上图可知hello.o是REL(可重定位)类型的文件。
4.3.2节头
描述了各个节的位置、大小、类型和偏移量等信息。
4.3.3符号表
存放在程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
首先通过objdump -d -r hello.o > hello.asm指令得到hello.o文件的反汇编格式,并与第3章的 hello.s进行对照分析。
将hello.s与hello.asm对比,可以看出两者的区别:
1.分支转移
hello.asm文件的跳转指令不使用段名称,而是使用确定地址。段名称只是汇编语言中为便于编写而引入,因此在机器语言中显然不存在。
2.函数调用
hello.s中的函数调用使用函数名称,hello.asm中call的目标地址则是当前下一条指令。此外,由于在编译阶段没有保留符号的名字,函数调用都被写为了<main+offset>的形式。
4.5 本章小结
汇编器接受汇编代码产生可重定位目标文件,本章在Ubuntu下生成了hello.o,hello.elf,hello.asm文件,并分别进行研究,观察机器语言的特性。同时将hello.s与hello.asm进行比较,了解汇编语言与机器语言的异同。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
5.1.1链接的概念
链接是指通过链接器(linker)将多个重定位目标文件整合,生成可执行文件(windows系统下为.exe文件,linux系统下一般省略后缀)的过程。
5.1.2链接的作用
链接使一个较大的程序可以分为多个模块,便于编写。
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的格式
首先通过readelf -a hello指令得到其ELF格式。
5.3.1ELF头
hello的ELF头与hello.o的ELF头基本相同。但是TYPE由REL(可重定向目标文件)变为DYN(共享对象文件);入口点(Entry point address)由未确定(0x0)变成了具体的地址(0x4010f0);程序头和节头的起始位置和大小都有改变;节头个数由14个变为了27个。
5.3.2节头
相比hello.o,hello的节头数目显著增加。
5.3.3程序头
程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。
5.3.4符号表
hello相较hello.o符号表多出了许多符号,而且额外有一张动态符号表(.dynsym)。此外,这些符号已经确定好了运行时位置。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息。
可见,hello的虚拟空间地址开始于0x401000,结束于0x401ff0。而根据5.3中节头表,可以通过edb找到各个节的信息。
5.5 链接的重定位过程分析
通过objdump -d -r hello指令得到hello的反汇编代码。
hello的反汇编代码相较hello.o多了很多节与函数,这是因为动态链接器将共享库中hello.c用到的函数加入到了可执行文件中。同时,hello.o中的相对偏移地址变成了hello中的虚拟内存地址。
根据以上分析,我们可知链接的过程就是链接器将所有的.o文件组装到一起,文件中的各个函数按照一定的顺序排列起来。
5.6 hello的执行流程
执行流程:
ld-2.31.so!_dl_start
ld-2.31.so!_dl_init
hello!_start
libc-2.31.so!_ libc_start_main
libc-2.31.so!__cxa_atexit
ld-2.31.so!_dl_init
hello!_libc_csu_init
hello!__init
libc-2.31.so!_setjmp
libc-2.31.so!_sigsetjmp
hello!main
hello!.plt+0x70
hello!printf@plt
hello!exit@plt
5.7 Hello的动态链接分析
查看elf文件可知:
动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。通过elf文件可知GOT起始表位置,在 edb中定位到该地址,如下图所示。
调用dl_init前:
调用dl_init后:
可以看到调用dl_init这两个位置的8个字节全部发生了改变。
5.8 本章小结
本章详细的阐述了链接,并且实现了将hello.o链接成为一个可执行文件。分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程,也与hello.o进行了对比。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1进程的概念
进程是一个执行中程序的实例。系统中的每个程序都在某个进程的上下文中。
6.1.2进程的作用
进程提供给应用程序的关键抽象:
1.一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
2.一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:是一个交互型应用级程序,代表用户运行其他程序。
处理流程:
1)从终端读入输入的命令
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序执行
5)shell 应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
用户输入运行命令后,shell判断其不是内置命令,于是将其判断为可执行程序,为其fork子进程:内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
6.4 Hello的execve过程
shell在fork子进程后调用execve加载并运行hello程序,需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。
3.映射共享区域。如果程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
6.5.1上下文信息
上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
6.5.2进程时间片
时间片是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间。
6.5.3进程调度
执行进程时,进程数一般都多于处理机数、这将导致它们互相争夺处理机。另外,系统进程也同样需要使用处理机。这就要求进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。
6.5.4用户态与核心态转换
为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
1.程序正常运行
2.程序运行时按下回车,会多打印几处空行,但依然可以正常运行
3.程序运行时按下Ctrl-Z,shell收到SIGSTP信号,挂起hello进程
4.程序运行时按下Ctrl-C,shell收到SIGINT信号,结束并回收hello进程
5.Ctrl-z后运行ps命令
6.Ctrl-z后运行jobs命令
7.Ctrl-z后运行pstree命令,可以将所有进程以树状图显示
8.Ctrl-z后运行fg 1命令,将hello进程再次调到前台执行
9.Ctrl-z后运行kill命令,则可以杀死指定进程
6.7本章小结
本章介绍了进程与Shell-bash的概念与作用。通过linux系统下的可执行文件hello实际地研究、展示了fork,execve函数地原理与执行过程,并了解了hello地各种异常与信号处理的结果。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1逻辑地址
逻辑地址(logical addresses)是由程序生成的与段相关的偏移地址的一部分。对hello.asm而言,其为其中的相对偏移地址。
7.1.2线性地址
线性地址(Linear address)是逻辑地址和物理地址转换之间的中间层。程序的代码会产生逻辑地址,或者说是段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。对hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。
7.1.3虚拟地址
即为上述线性地址。
7.1.4物理地址
物理地址(物理地址)是一个地址信号,表明在外部地址总线上选择的物理内存。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是应用在IA32架构上的管理模式。一组寄存器(CS,DS,SS等)保存着当前进程各段(如代码段、数据段、堆栈段)在描述符表中的索引,可以用来查询每段的逻辑地址。当获取了形如[aaaa:bbbb]的逻辑地址,可以通过简单的运算来取得线性地址(段基址*0x10H+段内偏移)。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,内存管理单元就必须查阅一个页表条目,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果页表条目碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在内存管理单元中包括了一个关于页表条目的小的缓存,称为翻译后备缓存器。
多级页表:将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。下图为k级页表进行翻译,利用前m位vpn1寻找一级页表位置,接着一次重复k次,在第k级页表获得了页表条目,将PPN与VPO组合获得PA。
7.5 三级Cache支持下的物理内存访问
物理地址分为标记,组索引和块偏移。首先,在L1 Cache中匹配组索引位,若匹配成功,则根据标记和偏移的匹配结果决定缺失或是命中。若组索引匹配不成功,则进入下一级Cache,重复直至进入内存。
7.6 hello进程fork时的内存映射
用户输入运行命令后,shell判断其不是内置命令,于是将其判断为可执行程序,为其fork子进程:内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。
7.7 hello进程execve时的内存映射
此节已在6.4中有详细阐述,因此不再赘述。
7.8 缺页故障与缺页中断处理
首先我们确定一个事情,页面命中是由硬件完成的,但是处理缺页却是硬件和操作系统协作完成的结果。
接下来我们再看一下整体的处理流程:
1.处理器生成一个虚拟地址,并将它传送给MMU
2.MMU生成PTE地址,并从高速缓存/主存请求得到它
3.高速缓存/主存向MMU返回PTE
4.如果试图进行的访问不合法,那么缺页处理程序会触发一个保护异常,从而终止这个进程。PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
5.缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
6.缺页处理程序页面调入新的页面,并更新内存中的PTE
7.缺页处理程序返回到原来的进程,再次执行导致缺页的命令。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU,此时,MMU就能正常的翻译A了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为一组不同大小的块的集合来维护。具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
7.10本章小结
本章从从段式管理,到三级cache的高层内存管理,再到程序内部的一个简单的堆的处理介绍了hello的存储管理。通过这章,更好地了解了虚拟内存相关的知识。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:
文件:所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。
设备管理:
Unix I/O接口:将设备映射为文件的方式允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等。
8.2 简述Unix IO接口及其函数
Unix IO接口的内容:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为 0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件 <unistd.h> 定义了常量 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO,它们可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
4.读写文件。一个读操作就是从文件复制n > 0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix IO接口的函数:
open函数:int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并返回描述符数字,若成功则为新文件描述符,若出错为-1。返回的描述符总是在进程中当前没有打开的最小描述符。
close函数:int close(int fd);
通过调用close函数关闭一个打开的文件,fd 是需要关闭的文件的描述符。若成功返回0,否则为-1。
read函数:ssize_t read(int fd, void *buf, size_t n);
read函数从描述符fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,返回值0表示EOF。否则返回值表示的是实际传送的字节数量。
write函数:ssize_t write(int fd, const void *buf, size_t n);
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。若成功则返回写的字节数,若出错则返回-1。
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型元素写入终端。
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;
}
首先,函数定义了指向缓冲区第一个位置的指针,然后调用了系统函数read,在该指针的位置读入字符。用户输入字符,触发中断,触发事件是键盘按下,通过系统调用获取ascii码,将对应的字符存入缓冲区,按下回车时,系统调用程序返回,并使getchar()返回读入的第一个字符。
8.5本章小结
本章介绍了linux下IO接口管理方法及其调用的函数,并对printf和getchar两个函数做了接口层面的分析。
(第8章1分)
结论
hello的程序生命周期以hello.c开始:
1.hello.c经过预处理,变成了保留预处理内容的文本文件hello,i;
2.hello.i经过编译,变成了汇编文件hello.s;
3.hello.s经过汇编,生成了保存二进制机器代码的可重定位目标文件hello.o;
4.hello.o通过与系统库进行链接,生成了可执行文件hello;
5.shell-bash进程调用fork函数,生成hello进程,又由execve函数加载运行当前进程上下文并运行程序hello;
6.hello在进程中通过多级页表、TLB等机制实现从虚拟内存(VA)到物理内存(PA)的转变,并运用动态内存,以达到和计算机交互的结果;
7.hello在运行时调用的函数中包含Unix I/O提供的函数,它用这些函数实现和I/O设备的交互;
8.hello运行结束,hello进程被shell父进程回收,释放内存并且删除有关的上下文。hello的一个程序生命周期至此结束。
经过一学期对计算机系统的学习,经历了4个实验、1个大作业,我认识到了计算机系统是一门及其具有深度的学科。从软件层面到硬件层面,再从程序语言到机器语言,计算机系统强调对计算机底层工作原理的理解。即便是最简单的一个hello.c文件,它的一个程序生命周期也是由许多复杂的底层过程组成,这正说明了计算机系统的重要性。无论在未来计算机能变得多么先进,计算机系统也依然不会过时。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名 | 作用 |
hello.c | C语言源程序,文本文件 |
hello.i | 经预处理的源程序,文本文件 |
hello.s | 经编译的汇编文件,文本文件 |
hello.o | 经汇编的可重定位目标文件,二进制文件 |
hello.elf | 经readelf得到的elf格式信息,文本文件 |
hello.asm | 经objdump得到的反汇编文件,文本文件 |
hello | 经链接得到的可执行目标文件,二进制文件 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E.Bryant, David O'Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4
[2] LINUX 基础命令和预处理[EB/OL].2017.4
https://www.csdn.net/tags/MtjaQg0sMTQ0NDMtYmxvZwO0O0OO0O0O.html
[3] 从用户态是怎么切换到核心态的?[EB/OL].2020.7
https://blog.csdn.net/qq_35590091/article/details/107314067
[4] Linux 用户态通过中断切换到内核态详解[EB/OL].2018.12
https://blog.csdn.net/weixin_36725931/article/details/85181264?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase
[5] 逻辑地址、线性地址和物理地址[EB/OL].2022.1
https://blog.csdn.net/Haomione/article/details/122731447
[6] Pianistx[EB/OL]
https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)