计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 信息安全
学 号 2022113554
班 级 2203201
学 生 陈宇童
指 导 教 师 史先俊
计算机科学与技术学院
2024年5月
本文通过分析hello的自白,解析hello程序从生成、预处理、编译、汇编、链接到生成进程后的进程管理、存储管理、IO管理的生命周期,与计算机系统课程内容和相关知识体系紧密联系。
关键词:hello;linux;P2P;020
目 录
第1章 概述
1.1 Hello简介
P2P:From Program to Process,意味着从程序到进程的过程。对于Hello的P2P则是从hello.c到hello产生进程的过程。Hello首先生成源代码hello.c,经过预处理器cpp预处理生成hello.i,再经过编译器cll编译生成hello.s,接着经过汇编器as汇编生成hello.o,再经过链接器ld链接生成可执行文件hello,在shell里输入./hello使用fork函数生成子进程。
020:From Zero-0 to Zero-0,意味着程序从开始到结束从无到无的过程。可执行文件hello开始运行,shell使用fork函数生成子进程并为子进程分配与父进程用户级虚拟地址空间相同但独立的副本和与父进程任何打开文件描述符相同的副本;在进程中调用execve函数在当前进程的上下文中加载并运行可执行文件hello,调用加载器将hello程序加载到当前进程的虚拟地址空间并映射到物理内存;加载动态链接后跳转到hello进程的入口函数_start()调用调用main()函数开始执行程序,程序执行后调用exit()函数退出;返回到wait()函数的调用点,回收子进程,内核删除子进程所有痕迹。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM
1.2.2 软件环境
Windows11 64位;VMware® Workstation 17 Pro;Ubuntu 22.04.3 LTS 64位
1.2.3 开发工具
Visual Studio 2022 64位;CodeBlocks 64位;vim;gcc;objdump;gdb;readelf
1.3 中间结果
中间结果文件的名字 | 文件的作用 |
hello.c | c语言代码源文件 |
hello.i | 经过预处理的文件 |
hello.s | 经过编译的汇编文件 |
hello.o | 经过汇编的可重定位目标文件 |
hello | 经过链接得到的二进制可执行目标文件 |
hello_o.elf | hello.o的elf格式 |
hello_o_asm.txt | hello.o反汇编得到的文本文件 |
hello.elf | hello的elf格式 |
hello_asm.txt | hello反汇编得到的文本文件 |
1.4 本章小结
本章解释了helloP2P和020的过程,介绍了本次大作业所用到的计算机系统环境与工具,提供了实验过程中间结果文件的名字与作业。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理是编译过程的预备阶段,在这一阶段,预处理器对源代码文件进行初步的文本处理。这包括宏定义的展开、文件的包含、条件编译指令的处理等。
预处理的作用:主要是对源代码进行清理和准备,使其变成纯净的、仅包含C/C++代码的文件,便于编译器后续的语法分析和编译。预处理器进行的工作有宏定义替换、文件包含处理、条件编译、移除注释、添加行号和文件名信息、生成预处理后的源代码。
2.2在Ubuntu下预处理的命令
Ubuntu下hello使用gcc的预处理指令为 gcc -E hello.c -o hello.i,如图2-1所示
图2-1 Ubuntu下hello使用gcc的预处理指令
2.3 Hello的预处理结果解析
hello的预处理结果hello.i如图2-2、图2-3所示
图2-2 生成hello.i
图2-3 hello.i部分内容
hello.i的解析:
如图2-4所示,hello.i由原本的包括注释的24行拓展到了3092行
图2-4 hello.i的尾部
如图2-3所示预编译过程对文件进行了文件包含处理,将头文件stdio.h、unistd.h和stdlib.h的内容插入到原本#include <stdio.h>、#include <unistd.h>、 #include <stdlib.h>所在的位置。
如图2-5、图2-6所示,预编译过程还引入了typedef数据类型别名和extern外部函数
图2-5 hello.i中的typedef
图2-6 hello.i中的extern
2.4 本章小结
本章介绍了预编译的概念与作用和在Ubuntu下预处理的命令,展示并分析了hello在Ubuntu下预处理的结果hello.i
第3章 编译
3.1 编译的概念与作用
编译的概念:编译是编译器前端(例如cc1)接收预处理后的代码,并将其转换成汇编代码。在此处编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序。
编译的作用:在编译阶段,编译器执行以下主要任务:①语法和语义检查:编译器检查代码是否符合C/C++语法规则,并进行语义分析,以确保代码没有逻辑错误。②生成中间表示:编译器将代码转换成中间表示形式,通常是一种低级的、与特定硬件无关的表示。③优化:编译器可能对中间表示进行优化,以提高程序的执行效率和代码质量。
3.2 在Ubuntu下编译的命令
Ubuntu下hello.i使用gcc的编译指令为 gcc -S hello.i -o hello.s,如图3-1所示
图3-1 Ubuntu下hello.i使用gcc的编译指令
3.3 Hello的编译结果解析
hello.i的编译结果hello.s如图3-2、图3-3所示
图3-2 生成hello.s
图3-3 hello.s部分内容
3.3.1数据
(1)常量
字符串常量"Hello %s %s %s\n"存储在.rodata段,如图3-4
图3-4 字符串在汇编代码中的形式
argc!=5的5为数字常量,以立即数形式存在于代码中,如图3-5
图3-5 常量5在汇编代码中的形式
(2)变量
该程序没有全局变量和静态变量。
该程序有三个局部变量i、argc、argv存储在栈中或直接存储在寄存器中。
由图3-5可知局部变量argc存储在-20(%rbp)中,局部变量i存储在如图3-6所示.L2的-4(%rbp)中。
图3-6 局部变量i的在汇编代码中的形式
(3)表达式
程序中有三个表达式argc!=5、i=0、i<10,在汇编代码中如图3-5第24行、图3-6第32行和图3-7第58行所示。
图3-7 i<10在汇编代码中的形式
(4)类型、宏
该程序没有定义新类型或宏。
3.3.2赋值
该程序有一个赋值语句i=0,如图3-6第32行所示。
3.3.3类型转换
该程序没有类型转换。
3.3.4算术操作
该程序有一个算术操作i++,如图3-8所示。
图3-8 i++在汇编代码中的形式
3.3.5逻辑/位操作
该程序没有逻辑/位操作。
3.3.6关系操作
该程序有两个关系操作argc!=5、i<10,分别如图3-5第24行、图3-7第58行所示。
3.3.7数组/指针/结构操作
该程序有一个数组操作argv,argv首地址为-32(%rbp)加上偏移量后得到argv[1]、argv[2]、argv[3]、argv[4],如图3-9所示。
图3-9 argv在汇编代码中的形式
3.3.8控制转移
程序中有两个控制转移操作,一个if/else操作,一个for操作,如图3-10、图3-11所示。
图3-10 if/else操作在汇编代码中的形式
图3-11 for操作在汇编代码中的形式
3.3.9函数操作
程序中有六个函数操作,分别是:main函数、exit函数、printf函数、sleep函数、atoi函数和getchar函数,函数返回值保存在寄存器%eax中。
(1)main函数
参数传递:main函数的参数为argc和argv[],在汇编代码中分别用%edi和%rsi存储,如图3-12所示。
函数返回:结束时main函数return0,在汇编代码中更改%eax为0,如图3-13所示。
图3-12 main函数的参数
图3-13 main函数的返回值
(2)exit函数
参数传递:exit函数的参数为常数1,在汇编代码中更改%edi为1,如图3-14所示。
图3-14 exit函数的参数传递
(3)printf函数
参数传递:printf函数调用了两次,参数分别为字符串常量、argv[1]与argv[2]和argv[3],在汇编代码中分别用%rdi和%rcx、%rdx、%rax存储,如图3-15、图3-9所示。
图3-15 printf调用字符串常量
(4)sleep函数
参数传递:sleep函数的参数为atoi(argv[4]),在汇编代码中用%edi存储,如图3-15所示。
图3-15 sleep的参数调用
(5)atoi函数
参数传递:atoi函数的参数为argv[4],在汇编代码中用%rdi存储,如图3-15所示。
函数返回:结束时atoi函数返回argv[4]的int形式,在汇编代码中用%edi存储,如图3-15所示。
(6)getchar函数
getchar函数没有参数
3.4 本章小结
本章介绍了编译的概念与作用和在Ubuntu下编译的命令,展示并分析了hello.i在Ubuntu下编译的结果hello.s
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。此处汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
汇编的作用:汇编的作用有①将汇编代码转换为机器代码:将汇编代码中的汇编指令翻译成特定硬件架构能理解的机器指令。②生成目标文件:生成一个或多个目标文件(.o文件),每个文件对应一个源文件或编译单元。
4.2 在Ubuntu下汇编的命令
Ubuntu下hello.s使用gcc的汇编指令为 gcc -c hello.s -o hello.o,如图4-1所示
图4-1 Ubuntu下hello.s使用gcc的汇编指令
4.3 可重定位目标elf格式
Ubuntu下hello.o使用readelf的汇编指令为 readelf -a hello.o > hello_o.elf,如图4-2所示
图4-2 Ubuntu下hello.o使用readelf的汇编指令
hello_o.elf文件大致可以分为ELF头部、节头、重定位节、符号表四部分。
4.3.1 ELF头部
hello_o.elf的elf头部包含16字节标识信息、文件类型、机器类型和节头表的表偏移、表项大小、表项个数等内容,如图4-3所示
图4-3 hello_o.elf的elf头
4.3.2 节头
hello_o.elf的节头部表包含节的类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐等描述文件节区的信息,如图4-4所示
图4-4 hello_o.elf的节头
4.3.3 重定位节
hello_o.elf的重定位节包含.text 节中需要进行重定位的信息,如图4-5所示。
图4-5 hello_o.elf的重定位节
4.3.4 符号表
hello_o.elf的符号表存了引用来自外部文件符号的全局符号和可执行文件的本地符号,如图4-6所示。
图4-6 hello_o.elf的符号表
4.4 Hello.o的结果解析
Ubuntu下hello.o使用objdump的反汇编指令为objdump -d -r hello.o > hello_o_asm.txt,如图4-7所示。
图4-7 Ubuntu下hello.o使用objdump的反汇编指令
hello_o_asm.txt文件内容,如图4-8、图4-9所示。
图4-8 hello_o_asm.txt文件内容
图4-9 hello_o_asm.txt文件内容
hello.s与hello.o反汇编得到的hello_o_asm.txt内容的对比与映射有
- 语句地址:hello.s中语句前无具体地址,hello_o_asm.txt中语句前有具体地址
- 数字常量:hello.s中数字用十进制表示,hello_o_asm.txt中数字用十六进制表示
- 控制转移:hello.s中控制转移地址用.L2、.L3、L4表示,hello_o_asm.txt中用相对偏移地址表示
- 函数操作:hello.s中函数操作用函数名称表示,hello_o_asm.txt中用相对函数偏移地址表示
4.5 本章小结
本章介绍了汇编的概念与作用和在Ubuntu下汇编的命令,分析了可重定位目标elf格式,展示并分析了hello.s在Ubuntu下编译的结果hello.o的反汇编内容。
第5章 链接
5.1 链接的概念与作用
链接的概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,由链接器来执行。此处的链接是指从 hello.o 到hello生成过程。
链接的作用:链接主要有三个作用:①接收一个或多个目标文件,以及所需的库文件,并将它们合并成最终的可执行文件。②解析目标文件中的符号引用,找到对应的符号定义,并将符号重定位,以便正确地指向它们的定义。③合并库文件,生成完整的可执行文件,其中包含所有的机器代码和解析后的符号。
5.2 在Ubuntu下链接的命令
Ubuntu下hello.o使用ld的链接指令为ld -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 /usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello,如图5-1所示
图5-1 Ubuntu下hello.o使用ld的链接指令
5.3 可执行目标文件hello的格式
Ubuntu下hello使用readelf的汇编指令为 readelf -a hello > hello.elf,如图5-2所示
图5-2 Ubuntu下hello使用readelf的汇编指令
hello.elf文件大致可以分为ELF头部、节头、程序头、重定位节、符号表五部分。
5.3.1 ELF头部
hello.elf的elf头部包含16字节标识信息、文件类型、机器类型和节头表的表偏移、表项大小、表项个数等内容,如图5-3所示
图5-3 hello.elf的elf头
5.3.2 节头
hello.elf的节头部表包含节的类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐等描述文件节区的信息,如图5-4所示
图5-4 hello.elf的节头
5.3.3 程序头
hello.elf的程序头定义了可执行文件在装入内存后的段内存布局,每个程序头对应进程的一个内存段,如图5-5所示
图5-5 hello.elf的程序头
5.3.4 重定位节
hello.elf的重定位节包含.text 节中需要进行重定位的信息,如图5-6所示。
图5-6 hello.elf的重定位节
5.3.5 符号表
hello.elf的符号表存了引用来自外部文件符号的全局符号和可执行文件的本地符号,如图5-7所示。
图5-7 hello.elf的符号表
5.4 hello的虚拟地址空间
使用edb加载hello,如图5-8所示。
图5-8 edb加载hello
根据5.3的节头信息,可以通过edb查看每个节的信息,如.text的起始地址为0xa5fc0100,如图5-9所示
图5-9 .text段
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
Ubuntu下hello使用objdump的反汇编指令为objdump -d -r hello > hello_asm.txt,如图5-10所示。
图5-10 Ubuntu下hello使用objdump的反汇编指令
hello_asm.txt文件部分内容,如图5-11所示。
图5-11 hello_asm.txt文件部分内容
hello.o反汇编得到的hello_o_asm.txt与hello反汇编得到的hello_asm.txt内容的对比有
- 语句地址:hello_o_asm.txt中使用的是相对偏移地址,如main函数地址为0;hello_asm.txt中使用保存虚拟内存地址对hello.o中的地址进行了重定位,main函数地址为0x11e9,如图5-12所示。
图5-12 hello_asm.txt中的main函数
- 函数操作:hello_asm.txt中加入了put、printf、getchar等函数的代码;hello_o_asm.txt中没有,如图5-13所示
图5-13 hello_asm.txt中的put等函数
- 初始代码和动态链接:hello_asm.txt中有.init节和.plt段,.init节用于程序初始化代码和初始化程序执行环境,.plt段为动态链接;而hello_o_asm.txt中没有,如图5-14所示
图5-14 hello_asm.txt中的.init和.plt
5.6 hello的执行流程
使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。
_init
.plt
puts@plt
printf@plt
getchar@plt
exit@plt
sleep@plt
_start
_dl_relocate_static_pie
main
__libc_csu_init
__libc_csu_fini
__fini
5.7 Hello的动态链接分析
Hello的动态链接器使用过程链接表plt和全局偏移量表got实现函数的动态链接。hello.elf的节表头中给出.plt和.got位置,得.got.plt起始位置为0x1090,如图5-15所示
图5-15 hello.elf的节表头给出的.plt和.got位置
5.8 本章小结
本章介绍了链接的概念与作用和在Ubuntu下链接的命令,展示并分析了hello.s在Ubuntu下编译的结果hello的反汇编内容,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程是正在运行的程序的实例。
进程的作用:进程为用户提供了假象:我们的程序好像是系统中当前运行的唯一程序 一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行 我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell的作用:shell 是一个交互型应用级程序,代表用户运行其他程序,负责各进程创建与程序加载运行及前后台控制,作业调用,信号发送与管理等。
处理流程:①读取用户从键盘输入的命令
②判断命令是否为内置命令
③若为内置命令则立即执行,否则调用相应的程序为其分配子进程并运行
④shell随时接受键盘输入信号,并进行相应处理
6.3 Hello的fork进程创建过程
在shell上输入./hello,命令行判断其不是内置命令,调用fork函数创建一个子进程,并为子进程分配与父进程用户级虚拟地址空间相同但独立的副本和与父进程任何打开文件描述符相同的副本和PID,在子进程中执行程序hello。
6.4 Hello的execve过程
在进程中调用execve函数在当前进程的上下文中加载并运行可执行文件hello,即删除已存在的用户区域、映射私有区、映射共享区、设置PC,后调用加载器将hello程序加载到当前进程的虚拟地址空间并映射到物理内存。
6.5 Hello的进程执行
进程上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
进程时间片:一个进程执行它的控制流的一部分的每一个时间段叫做时间片,多任务也叫时间分片。
进程执行过程如图6-1所示
图6-1 进程执行过程
hello的进程调度:一开始,hello处于循环状态,hello调用sleep函数将内核抢占申请进程休眠,挂起hello,设置倒计时,倒计时结束产生中断信号中断当前休眠进程,执行上下文切换恢复hello的上下文信息,继续执行hello;循环结束后,调用getchar函数,hello由用户模式转向内核模式,进行陷阱处理程序请求来自键盘的输入,上下文切换执行键盘输入的信号代表的进程;完成键盘输入信号后,产生中断信号,内核将控制从信号代表的进程转回hello;最后return,进程终止,回收子进程。
6.6 hello的异常与信号处理
hello执行过程中会出现四类异常:中断、陷阱、故障和终止。其处理方式如图6-2所示
图6-2 异常处理
程序正常运行:完成后被回收
图6-3 程序正常运行
程序输入ctrl+z:程序停止,hello暂时挂起
图6-4 输入ctrl+z
图6-5 hello挂起
程序输入ctrl+c:程序停止,hello被回收
图6-6 输入ctrl+z
图6-7 hello回收
程序输入kill:程序被杀死
图6-8 输入kill
6.7本章小结
本章介绍了进程的概念与作用,简述壳Shell-bash的作用与处理流程,讲述了hello的进程创建、执行、终止的过程以及异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:是程序经过编译后出现在汇编代码中的地址,用来指定一个操作数或者是一条指令的地址,由一个段标识符和一个指定段内相对地址的偏移量组成。
线性地址:逻辑地址经过段机制后转化为线性地址,由描述符、偏移量的组成。
虚拟地址:相对于进程地址空间由逻辑地址或线性地址生成,是一个抽象的地址,它提供给进程使用,不代表真实的物理存储器中的位置。
物理地址:计算机系统中实际的存储器地址,是地址变换的最终结果地址,是指计算机中存储设备中某个特定位置的物理位置。
7.2 Intel逻辑地址到线性地址的变换-段式管理
分段过程的实质就是逻辑地址到线性地址的过程。
逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏移量组成。段首地址存放在段描述符中,段描述符存放在全局描述符表GDT或局部描述符表LDT中。TI指示段描述符是在GDT还是LDT中,索引指示段描述符在段描述符表中的位置。
段式管理先通过段选择符的指示,在段描述符表中找到对应的段描述符,后从段描述符中获得段首地址与逻辑地址中的偏移量相加得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址,即虚拟地址包含虚拟页号VPN、虚拟页面偏移VPO两部分。
MMU先从虚拟地址中得到VPN,并且检查TLB是否缓存了PTE的一个副本。使用VPN中TLB索引和TLB标记,从TLB中找对应组中是否有匹配的条目。若命中,将缓存的PPN返回给MMU;若不命中,MMU需从PTE中取出PPN,若PTE有效,则取出PPN,若PTE无效或不匹配,则缺页,内核需调入所需页面重新加载。最后VPO与PPN相连接即为对应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
CPU产生虚拟地址VA,传给MMU,MMU使用前36位作为VPN,在TLB中搜索。如果TLB命中,将其中的物理页号PPN与VPO合并成物理地址PA;如果TLB没有命中,就向内存中的页表请求PTE。
CR3是L1 PTE的起始地址,如果L1 TLB命中,将其中的物理页号PPN与VPO合并成物理地址PA;如果L1 TLB没有命中,就向L2中的页表请求PTE;VPN1是L1 PTE中的偏移量,用来确定L2 PTE的起始地址,得到L2 PTE的起始地址,VPN2又作为L2 PTE的偏移量。如果一直不命中,以此类推访问下一级页表,最后在L4 PTE中找到对应的PPN,与VPO组合成物理地址PA。
7.5 三级Cache支持下的物理内存访问
先使用物理地址VA的CI进行组索引,对组内每一路的块分别通过CT进行标志位匹配,若匹配成功且块的valid标志位为1,则hit,根据CO取出数据并返回;若匹配不成功的或标志位为0,则miss,cache向下一级cache,中寻找查询数据,逐级写入cache。在更新cache的时候采用LRU策略。
7.6 hello进程fork时的内存映射
hello调用fork函数创建一个子进程,并为子进程创建了当前进程的mm_struct、区域结构和页表的原样副本,分配与父进程用户级虚拟地址空间相同但独立的副本和与父进程任何打开文件描述符相同的副本和唯一的PID,在子进程中执行程序hello。两个进程每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork返回时,新进程的虚拟内存和调用fork时存在的虚拟内存相同,两个进程中的任何一个进行写操作时,写时复制机制就会创建新页面,每个进程都保持了私有地址空间。
7.7 hello进程execve时的内存映射
hellod调用fork函数创建子进程后,调用execve函数在上下文加载hello程序,经历四个过程:①删除当前虚拟地址中已存在的用户区域。②映射私有区域,为新程序的代码、数据、bss和堆栈区域创建新的区域结构。③映射共享区域,hello程序与共享对象libc.so链接映射到用户虚拟地址空间中的共享区域内④设置程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
当程序出现缺页故障与缺页中断,处理器生成虚拟地址,传送给MMU;MMU翻译出PTE地址,并在高速缓存中查找PTE;高速缓存将PTE返还给MMU,PTE中有效位是0,引发缺页异常,调用缺页异常处理程序;该程序选择一个牺牲页把它换到磁盘,调入新的页面,更新内存中的PTE,返回到原来的进程,再次执行导致缺页的命令。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。
分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。
分配器有两种基本类型。两种类型都是要求显示的释放分配块。
显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器;
隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块,也叫垃圾收集器。
7.10本章小结
本章介绍了hello的存储器地址空间、段式管理、页式管理、TLB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问;分析了hello进程fork、execve时的内存映射和缺页故障与缺页中断处理、动态内存管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
Linux所有的I/O设备都被模型化为文件,所有的输入和输出是对文件的读和写。
设备管理:unix io接口
一个简单、低级的应用接口,使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix中输入和输出的执行:
(1)打开文件:应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个很小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关打开文件的所有信息,应用程序只需记住这个描述符。描述符:标准输入(0),标准输出(1),标准错误(2)
(2)改变当前的文件位置:对于每个打开的文件,内核C保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
(3)读写文件:读操作就是从文件拷贝n>0个字节到存储器,从当前文件位置k开始,然后将k增加到k+n。写操作就是从存储器拷贝n>0个字节到文件,从当前文件位置k开始,然后更新k。
(4)关闭文件:应用完成对文件的访问够,它通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池当中。
Unix IO函数
(1)int open(char *filename, int flags, mode_t mode);
将filename转换为一个文件描述符,若成功返回描述符数字,返回的描述符总是在进程中没有打开的最小描述符;否则返回-1。flags参数指明进程打算如何访问这个文件,也可以为写提供一些额外的指示;mode参数指定新文件的访问权限位。
(2)int close(int fd);
通知内核结束访问一个文件,关闭打开的一个文件。成功返回0,否则返回-1。
(3)ssize_t read(int fd, void *buf, size_t n);
从描述符为fd的当前文件位置最多复制n个字节到内存位置buf。返回值-1表示出错,返回值0表示EOF,否则返回值表示的是实际传送的字节数量。
(4)ssize_t write(int fd, const void *buf, size_t n);
从内存位置buf最多复制n个字节到描述符为fd的当前文件位置。返回值-1表示出错,否则返回值表示的是实际传送的字节数量。
8.3 printf的实现分析
(1)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为字符指针。
(2)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,产生格式化输出。
(3)write函数的汇编代码:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
其中int INT_VECTOR_SYS_CALL表示通过陷阱-系统来调用sys_call函数。write的作用是把buf中i个元素写到终端。
printf函数的实现过程为: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;
}
其中,BUFSIZ为缓冲区的最大长度,bb为指针指向缓冲区的首地址。getchar函数实现过程:调用read函数,将缓冲区读入到buf中,将长度送给n,再令bb指针指向buf,返回buf中的第一个字符,如果长度n < 0,则报EOF错误。getchar被调用时,调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
8.5本章小结
本章介绍了Linux的IO设备管理方法,简述了unix io接口及其函数,分析了printf、getchar的实现。
结论
hello所经历的过程:
- 编写:c语言编写hello.c
- 预处理:hello.c经过预处理器cpp预处理生成hello.i
- 编译:hello.i经过编译器cll编译生成hello.s
- 汇编:hello.s经过汇编器as汇编生成hello.o
- 链接:hello.o经过链接器ld链接生成可执行文件hello(.out)
- 创建子进程:在shell里输入./hello使用fork函数生成子进程
- 加载并运行程序:在进程中调用execve函数在当前进程的上下文中加载并运行可执行文件hello
- 存储管理:内存管理单元MMU将hello程序加载到当前进程的虚拟地址空间并映射到物理内存
- IO管理:运行时调用linux I/O设备相关函数,例如printf函数
- 回收子进程:程序返回到wait()函数的调用点,回收子进程,内核删除子进程所有痕迹。
感悟:计算机系统是一门相当复杂且系统化的课程,内容繁杂却又紧密联系,仅是hello程序的生命周期内便涉及到一系列(Editor+Cpp+Compiler+AS+LD+OS+ CPU/RAM/IO)操作,让我意识到自身学习的浅薄,更意识到系统化学习的重要性。
附件
中间产物的名字 | 文件的作用 |
hello.c | c语言代码源文件 |
hello.i | 经过预处理的文件 |
hello.s | 经过编译的汇编文件 |
hello.o | 经过汇编的可重定位目标文件 |
hello | 经过链接得到的二进制可执行目标文件 |
hello_o.elf | hello.o的elf格式 |
hello_o_asm.txt | hello.o反汇编得到的文本文件 |
hello.elf | hello的elf格式 |
hello_asm.txt | hello反汇编得到的文本文件 |
参考文献
[1] CSDN. C语言编译过程中*.i *.s *.o *.out 等文件是什么?[EB/OL] (2020-12-23) [2024-06-01] https://blog.csdn.net/Groupers/article/details/111570459
[2] 阿里云. C/C++编译的第一步:深入了解预处理器的力量与优化. [EB/OL] (2024-03-27) [2024-06-01] https://developer.aliyun.com/article/1469007
[3] CSDN. 从C源代码到可执行文件的四个过程:预处理、编译、汇编、链接. [EB/OL] (2021-09-01) [2024-06-01] https://blog.csdn.net/weixin_44966641/article/details/120042638
[4] CSDN. GCC编译过程:预处理->编译->汇编->链接. [EB/OL] (2023-08-07) [2024-06-01] https://blog.csdn.net/weixin_57082854/article/details/132152404?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-5-132152404-blog-81221777.235^v43^pc_blog_bottom_relevance_base6&spm=1001.2101.3001.4242.4&utm_relevant_index=8
[5] 博客园. Linux二进制分析-ELF格式分析及符号重定位. [EB/OL] (2021-03-04) [2024-06-02] https://www.cnblogs.com/bunner/p/14481560.html
[6] CSDN. ELF格式文件详细分析. [EB/OL] (2017-06-12) [2024-06-02] https://blog.csdn.net/xuehuafeiwu123/article/details/72963229
[7] CSDN. 进程的上下文切换. [EB/OL] (2021-11-08) [2024-06-02] https://blog.csdn.net/qq_39918677/article/details/120403960