程序人生-Hello’s P2P
https://i-blog.csdnimg.cn/direct/51849c31f0df4f41b9897195cd5cd0f9.jpeg
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 数学与应用数学+计算机科学与技术
学 号 2023110151
班 级 23SXS11
学 生 孙灿阳
指 导 教 师 史先俊
计算机科学与技术学院
2024年5月
摘 要
hello程序最早诞生在源代码中,在履历了一系列变革后,变成了一个可执行文件,然后在进程中被执行,当程序执行完毕后,进程退出,hello的生命彻底竣事。本文通过对hello的一生的探索,详细介绍了预处理、编译、汇编、链接、进程管理、存储管理、IO管理等方面的内容。
关键词:编译;链接;进程;存储管理;IO
目 录
第1章 概述................................................................................... - 4 -
1.1 Hello简介............................................................................ - 4 -
1.2 情况与工具........................................................................... - 4 -
1.3 中间效果............................................................................... - 4 -
1.4 本章小结............................................................................... - 4 -
第2章 预处理............................................................................... - 5 -
2.1 预处理的概念与作用........................................................... - 5 -
2.2在Ubuntu下预处理的命令................................................ - 5 -
2.3 Hello的预处理效果解析.................................................... - 5 -
2.4 本章小结............................................................................... - 5 -
第3章 编译................................................................................... - 6 -
3.1 编译的概念与作用............................................................... - 6 -
3.2 在Ubuntu下编译的命令.................................................... - 6 -
3.3 Hello的编译效果解析........................................................ - 6 -
3.4 本章小结............................................................................... - 6 -
第4章 汇编................................................................................... - 7 -
4.1 汇编的概念与作用............................................................... - 7 -
4.2 在Ubuntu下汇编的命令.................................................... - 7 -
4.3 可重定位目的elf格式........................................................ - 7 -
4.4 Hello.o的效果解析............................................................. - 7 -
4.5 本章小结............................................................................... - 7 -
第5章 链接................................................................................... - 8 -
5.1 链接的概念与作用............................................................... - 8 -
5.2 在Ubuntu下链接的命令.................................................... - 8 -
5.3 可执行目的文件hello的格式........................................... - 8 -
5.4 hello的虚拟地址空间......................................................... - 8 -
5.5 链接的重定位过程分析....................................................... - 8 -
5.6 hello的执行流程................................................................. - 8 -
5.7 Hello的动态链接分析........................................................ - 8 -
5.8 本章小结............................................................................... - 9 -
第6章 hello进程管理.......................................................... - 10 -
6.1 进程的概念与作用............................................................. - 10 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 10 -
6.3 Hello的fork进程创建过程............................................ - 10 -
6.4 Hello的execve过程........................................................ - 10 -
6.5 Hello的进程执行.............................................................. - 10 -
6.6 hello的异常与信号处理................................................... - 10 -
6.7本章小结.............................................................................. - 10 -
第7章 hello的存储管理...................................................... - 11 -
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 -
7.9动态存储分配管理.............................................................. - 11 -
7.10本章小结............................................................................ - 12 -
第8章 hello的IO管理....................................................... - 13 -
8.1 Linux的IO设备管理方法................................................. - 13 -
8.2 简述Unix IO接口及其函数.............................................. - 13 -
8.3 printf的实现分析.............................................................. - 13 -
8.4 getchar的实现分析.......................................................... - 13 -
8.5本章小结.............................................................................. - 13 -
结论............................................................................................... - 14 -
附件............................................................................................... - 15 -
参考文献....................................................................................... - 16 -
第1章 概述
1.1 Hello简介
[*]
[*]
[*]P2P:从程序到进程
1.1.1.1 程序阶段(Program)
Hello程序最早诞生在源代码中。我们有一个hello.c文件,这个文件是用高级编程语言C语言编写的,而且在此时它只是一个静态的、不可执行的代码文本。可以把它看作是一个“静止的实体”,没有任何动态执行本领。
1.1.1.2 预处理、编译、汇编、链接
这个源代码在履历了预处理、编译、汇编和链接的步调后,变成了一个可执行文件。在这些过程中,程序源代码被转换为机器语言,形成一个二进制文件,它包罗了计算机能够理解的指令,但程序本身并没有真正运行。
1.1.1.3 进程阶段(Process)
当用户在操纵系统中执行这个二进制可执行文件时,操纵系统会利用fork()和execve()来创建一个新的进程。此时,程序从静态代码变成了一个进程。进程会获得系统资源(如内存、CPU时间片)并开始执行。
在这个过程中,操纵系统的进程管分析分配时间片,调度该进程在CPU上执行,并确保它有充足的内存(通过虚拟内存管理)。同时,操纵系统会通过内存管理单元(MMU)为进程分配虚拟地址(VA)并映射到物理内存地址(PA)。假如程序访问的数据不在缓存中,操纵系统会通过页表和TLB等机制进行管理,确保高效的数据访问。
操纵系统会通过各种I/O管理机制(如文件系统、硬件设备驱动程序等)来包管程序能够顺遂访问硬盘、表现器、键盘等外设。
1.1.2 O2O:从Zero-0到Zero-0
Hello的O2O过程可以被看作是程序从无到有,再从有到无的一个完整生命周期。O2O即Zero-to-Zero,即从“零”开始到“零”竣事。这个过程是简洁而深刻的,涉及计算机系统怎样执行一个简朴程序的整个生命周期。
1.1.2.1 Zero-0:从程序的源代码开始,这时,源代码只是静态存在的文件,它并没有任何运行本领。它相称于是一个零的状态,没有经过任那边理,不能直接被计算机执行。
1.1.2.2 准备执行
在写完源代码后,需要将其转换成计算机能够执行的机器代码。这个过程经过以下几个步调:预处理——处理全部宏界说和包罗文件等;编译——将源代码转换为中间代码;汇编——将汇编语言转换为机器语言;链接——将全部目的文件和库文件链接成一个完整的可执行文件。这些步调把程序从原始的零转化成了一个可以执行的文件,但它依然还不是进程。
1.1.2.3 从0到1
当用户决定运行该程序时,操纵系统会加载可执行文件,并将其变成一个进程。此时,程序履历了从静态的代码到动态的进程的转变。操纵系统为这个进程分配CPU时间、内存空间和其他硬件资源。操纵系统中的进程管理模块开始为该进程分配时间片并调度它执行。
1.1.2.4 回到0
当程序完成输出并竣事时,操纵系统会整理进程的资源,开释内存,关闭文件形貌符等,进程会正常退出。此时,进程状态变为“停止”,系统将其从内存中移除。这时,Hello的“生命”彻底竣事,重新回到0。
1.2 情况与工具
硬件情况:X64 CPU;2.20GHz;16.0GB RAM
软件情况:Windows11;VMware Workstation Pro 17;Ubuntu 20.04
开辟与调试工具:gcc, edb, objdump, readelf
1.3 中间效果
名字
作用
hello.i
对hello.c预处理后的文件
hello.s
对hello.i编译后生成的文本文件
hello.o
对hello.s汇编后生成的可重定位目的文件
hello
链接后生成的可执行目的文件
1.elf
hello.o的ELF文件
2.asm
hello.o的反汇编文件
3.elf
hello的ELF文件
4.asm
hello的反汇编文件
1.4 本章小结
本章对hello、情况与工具、中间效果做了一个介绍,为反面使命的完成做准备。我对hello的一生有了一个清晰的熟悉,从hello.c开始,经过预处理、编译、汇编、链接生成可执行文件hello,然后在各方的帮助下开始在进程中被执行,真正获得了生命,执行完毕后,又规复了最开始的“0”状态。可以说,hello既是from program to progress,又是from zero to zero。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理是源代码编译过程中第一个步调,它发生在实际的编译之前。在C语言或C++等语言的编译过程中,预处理器(如gcc的cpp)会对源代码进行处理,生成最终的代码供编译器进一步编译。预处理器处理的是源代码中的一些指令,这些指令以#开头,因此被称为预处理指令。
2.1.2 预处理的作用
2.1.2.1 宏界说和替换
预处理器会处理全部以#define界说的宏。宏是可以在程序中多次利用的代码片段,预处理器会在编译之前将宏名称替换为宏界说的内容。
2.1.2.2 文件包罗(#include)
#include指令用于将其他文件(通常是头文件)包罗到当前源文件中。预处理器会将包罗的文件的内容插入到指定位置。
2.1.2.3 条件编译(#ifdef, #ifndef, #else, #endif)
条件编译答应程序员根据差异的条件来编译差异的代码部分。它常用于跨平台开辟和调试。
2.1.2.4 宏函数(#define 的扩展)
预处理器不仅可以界说常量,还可以界说带参数的宏,这些宏在利用时会展开成函数代码。
2.1.2.5 文件保护(#ifndef, #define, #endif)
预处理器可以防止头文件被重复包罗。例如,头文件中的代码可能会在多个地方被#include,为了防止重复界说,常利用宏来控制文件的多重包罗。
2.1.2.6 替换常量和代码片段
预处理器可以替换常量值、简化复杂的计算。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
https://i-blog.csdnimg.cn/direct/c35240ea031f4375b8490f0d09677acc.png
图2-1 预处理过程
2.3 Hello的预处理效果解析
https://i-blog.csdnimg.cn/direct/beccac44a2324df48fd30cc9708805b4.png
图2-2 hello.i(部分)
得到了另一个C程序——hello.i,一共有3061行。这是因为预处理器读取了系统头文件的内容,并把它直接插入程序文本中,比如stdio.h。末了的3047行到3061举动原始的C程序。
2.4 本章小结
预处理是源文件到目的文件的转化过程中非常重要的一步,它通过宏界说、条件编译、文件包罗等机制,在编译前对原始的C程序进行修改,从而为后续的编译阶段提供了更合适的代码。通过查看hello.i,我对预处理有了更清晰的熟悉。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译器(cc1)将预处理后的源代码hello.i转换为目的架构的汇编代码hello.s,它包罗一个汇编语言程序。
3.1.2 编译的作用
3.1.2.1 语法解析与翻译
在此阶段,编译器会对hello.i(预处理后的C语言代码)进行语法分析,确保代码符合目的语言(C语言)的语法规范。编译器会将C语言代码转化为对应的汇编语言代码,即hello.s文件。
语法树构建:编译器会通过抽象语法树(AST)来表现源代码的布局,从而理解程序的逻辑布局。
指令生成:编译器根据目的架构的指令集(如x86、ARM等)生成汇编指令。每个C语言表达式(如加法、赋值、函数调用等)都会对应一条或多条汇编指令。
3.1.2.2 将高层语言转化为低层语言
编译的作用之一是将高级语言(如C)转化为计算机能够理解并执行的低级语言。固然C语言语法靠近人类的语言,但计算机只能执行机器语言或汇编语言。通过编译,从hello.i到hello.s的转换使得程序更靠近硬件。
这个过程涉及对数据范例、表达式、控制布局(如循环、条件语句)等的低级表现。例如,C语言中的一个printf()函数调用,在汇编语言中可能会变成一条系统调用或库函数调用,并通过相应的指令在计算机上执行。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
https://i-blog.csdnimg.cn/direct/0d7a32c2ae0a42739948e3d1d9fbd7c9.png
图3-1 编译过程
3.3 Hello的编译效果解析
3.3.1 数据
3.3.1.1 常量
hello.c中的5和1在hello.s中被表现为立刻数$5和$1。
https://i-blog.csdnimg.cn/direct/eb072e35527b4b9c9c48be64caba5a27.png
图3-2
https://i-blog.csdnimg.cn/direct/4a651b190c8c4d2f8d51379a88710387.png
图3-3
printf中的字符串位于.rodata。
https://i-blog.csdnimg.cn/direct/ed56ab93ccb542e9979cbc5de68e0db5.png
图3-4
3.3.1.2 变量
局部变量i位于栈中。
https://i-blog.csdnimg.cn/direct/5e28cccbd66644cbb8089e2b181beec8.png
图3-5
3.3.2 赋值
hello.c中的i = 0在hello.s中用movl实现。
https://i-blog.csdnimg.cn/direct/3f97c83815b24e49ba6c9337b0f91641.png
图3-6
3.3.3 范例转换
调用atoi函数对输入的秒数进行范例转换。
https://i-blog.csdnimg.cn/direct/ac6474e36ff24d33b75ba3fb075a9946.png
图3-7
3.3.4 算数操纵
hello.c中的i++在hello.s中用addl实现。
https://i-blog.csdnimg.cn/direct/bcca1373358e4af89fe48fbedcf145a8.png
图3-8
3.3.5:关系操纵
hello.c中的argc!=5和i<9在hello.s中用cmpl来实现。
https://i-blog.csdnimg.cn/direct/cdb7c388a1ba458b8a8d329e240df89d.png
图3-9
https://i-blog.csdnimg.cn/direct/4cef978ba5bb4c6ab27c969595bc4d46.png
图3-10
3.3.6 数组/指针/布局操纵
argv的地址为-32(%rbp),两个相邻元素的地址相差8,因此对-32(%rbp)加24,加16,加8,加32来计算另外4个元素的地址,然后进行读取。
https://i-blog.csdnimg.cn/direct/9dc197229a134cd38b6f63fc0a498290.png
图3-11
3.3.7 控制转移
if(argc!=5):
在hello.s中,cmpl对argc和5进行比力并设置条件码,je根据条件码选择是否跳转。
https://i-blog.csdnimg.cn/direct/9075ec9ed34f42bfa544c7bc815b4630.png
图3-12
for(i=0;i<9;i++):
在hello.s中,cmpl对i和8进行比力并设置条件码,jle根据条件码选择是否跳转。
https://i-blog.csdnimg.cn/direct/e81c5ba23f8c4aaea5daeb1e2b8a3f5d.png
图3-13
3.3.8 函数操纵
3.3.8.1 参数通报
假如参数小于等于6个,则全部用寄存器通报参数;假如参数大于6个,则6个参数用寄存器通报,剩下的参数则存储在栈中。
3.3.8.2 函数调用
printf(“用法:Hello 学号 姓名 手机号 秒数!\n”)
在hello.s中,通过调用puts@PLT来实现打印。
https://i-blog.csdnimg.cn/direct/4ded4b2816bd40b987588921a21f1c16.png
图3-14
exit(1)
在hello.s中,通过调用exit@PLT来实现。
https://i-blog.csdnimg.cn/direct/f7e335c1b42e43f1ac0c6b00ed6e539e.png
图3-15
printf(“Hello %s %s %s \n”, argv, argv, argv)
argv的地址为-32(%rbp),两个相邻元素的地址相差8,因此对-32(%rbp)加24,加16,加8来计算3个参数的地址,然后进行读取。然后调用printf@PLT进行打印。
https://i-blog.csdnimg.cn/direct/e45ee68cea484999b8fedd49cd8018d4.png
图3-16
sleep(atoi(argv))
对-32(%rbp)加32来计算参数的地址,然后进行读取,再调用atoi@PLT进行转换,末了调用sleep@PLT进行休眠。
https://i-blog.csdnimg.cn/direct/aec34cdf8da44004b24a4d1936cbb379.png
图3-17
getchar()
在hello.s中,通过调用getchar@PLT来读取字符。
https://i-blog.csdnimg.cn/direct/0753ae5b458f4f95a5237733d5d7cb3e.png
图3-18
3.3.8.3 函数返回
在hello.s中,指令ret用来实现函数返回。
https://i-blog.csdnimg.cn/direct/39e8033e19294ad0b4ce50831eddd1a2.png
图3-19
3.4 本章小结
编译器(cc1)将预处理后的源代码hello.i转换为目的架构的汇编代码hello.s,通过解析hello的编译效果(数据、赋值、范例转换、算数操纵、逻辑/位操纵、关系操纵、数组/指针/布局操纵、函数操纵),我阅读汇编代码的本领得到了很大的提拔,对编译有了更深的理解。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是将汇编语言代码转换为机器语言的过程,它是计算机程序编译过程中的一个重要步调。汇编语言是一种低级语言,它直接对应于计算机硬件的指令集架构,比高级编程语言更靠近硬件。汇编器将汇编语言源代码转换为机器代码,这使得计算机能够执行该程序。
4.1.2 汇编的作用
4.1.2.1 将汇编语言转换为机器语言
机器语言是计算机能够直接执行的唯一语言,通常由二进制指令构成。汇编语言固然靠近机器语言,但依然由符号组成,需要通过汇编器转换成机器语言。汇编语言中的每条指令通过汇编器转换为特定硬件架构的机器指令。机器指令是计算机CPU能够辨认并执行的原始指令。
4.1.2.2 提供更细粒度的控制
汇编语言比高级语言更靠近硬件,因此它答应程序员对硬件的控制更细致。程序员可以直接操纵CPU的寄存器、内存地址,甚至控制特定硬件设备的举动。在性能要求极高的场景下(如操纵系统内核、嵌入式系统、驱动程序等),汇编语言常用于优化程序性能,确保代码能以最精确、最高效的方式与硬件交互。
4.1.2.3 代码优化和性能提拔
利用汇编语言可以进行低级别的优化,例如利用特定CPU指令集架构的上风,避免高级语言编译器可能产生的冗余代码。汇编代码能够精确控制每一个操纵,避免一些高级语言编译器生成的额外开销。在极端的性能要求下,编写汇编代码可以显著提拔程序执行效率,特殊是在嵌入式系统和对实时性要求严格的应用场所。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
https://i-blog.csdnimg.cn/direct/6d727cb44cdb4956a61da305f496ae71.png
图4-1 汇编过程
4.3 可重定位目的elf格式
https://i-blog.csdnimg.cn/direct/f38e3de5a479487490a58d1e21b078b7.png
图4-2 典范的ELF可重定位目的文件
首先获得hello.o的ELF文件
https://i-blog.csdnimg.cn/direct/9a9bb97155e04eefb2e4b32b361969aa.png
图4-3
4.3.1 ELF头
https://i-blog.csdnimg.cn/direct/55707d44eae74888af9b7c0af460463f.png
图4-4 ELF头
从ELF头可以看到:数据为小端序,文件范例为可重定位文件,入口点地址为0,没有程序头,节头开始处为1264,ELF头的大小为64字节,程序头的大小为0,程序头的数量为0,节头大小为64比特,节头数量为14。
4.3.2 节头
https://i-blog.csdnimg.cn/direct/9ac3443e784f4b79b0abbd368eddd1a2.png
图4-5 节头
节头包罗差异节的名称、范例、地址、偏移量、大小等信息。
4.3.3 重定位节.rala.text和.rela.eh_frame
https://i-blog.csdnimg.cn/direct/df9a4fb54cc44450852f5e62629b6b4d.png
图4-6 .rela.text和.rela.eh_frame
.rel.text是一个重定位节,通常用于存储与.text节(存储代码)的重定位相干的数据。当目的文件与其他文件组合时,它告诉链接器需要怎样调解.text节中某些位置的指令或数据,确保程序在运行时能够精确调用函数、访问变量等。
.rela.eh_frame与.rel.text雷同,它与.eh_frame节中的异常处理框架数据相干,用于存储指向.eh_frame中数据的符号重定位信息。
4.3.4 .symtab
https://i-blog.csdnimg.cn/direct/401732fd1794424693b88fca1c58f072.png
图4-7 .symtab
在ELF文件格式中,.symtab节是一个非常重要的部分,它存储了程序中的符号信息。符号通常代表程序中的变量、函数、对象或其他可以由链接器、调试器等工具辨认的元素,因此,.symtab节对于链接和调试等过程至关重要。
4.4 Hello.o的效果解析
首先得到hello.o的反汇编文件。
https://i-blog.csdnimg.cn/direct/89d5326cb43b4b6a8d7b81c34af62f2b.png
图4-8 反汇编过程
https://i-blog.csdnimg.cn/direct/dc5783c60bb949d594a3c17bb4ccba54.png
图4-9 2.asm
4.4.1 机器语言的构成
机器语言是计算机能够直接理解和执行的指令集,它由一系列的二进制位组成。每个机器指令通常由多个字段组成,用于指定操纵码(操纵符)、操纵数以及其他控制信息。
4.4.2 与汇编语言的映射关系
汇编语言与机器语言之间的映射关系可以通过汇编器来实现,汇编器将汇编语言转换为机器语言。每条汇编指令对应一条机器语言指令。
4.4.3 分支转移的差异
对于if(argc!=5),2.asm和hello.s在跳转时不太一样。
https://i-blog.csdnimg.cn/direct/e9130f2d01f4448a975cf340dddbe91a.png
图4-10 2.asm
https://i-blog.csdnimg.cn/direct/f190c0a01c4041ad9f38d7823c6c1698.png
图4-11 hello.s
4.4.4 函数调用的差异
对于exit(1),2.asm和hello.s在处理时不太一样。
https://i-blog.csdnimg.cn/direct/0e570656e6e949f18a70d3223e67aee5.png
图4-12 2.asm
https://i-blog.csdnimg.cn/direct/f5950222c3484eae96153dd3c96ba428.png
图4-13 hello.s
4.5 本章小结
汇编器将汇编语言源代码转换为机器代码,这使得计算机能够执行该程序。通过查看hello.o的ELF文件,我对汇编有了更深入的相识。通过比力2.asm和hello.s,我看到了机器语言与汇编语言的联系与区别。
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是程序开辟中的一个重要过程,它是将源代码编译后的对象文件(.o文件)和库文件(如静态库、动态库)组合成最终可执行文件或共享库的过程。链接的重要使命是处理代码和数据之间的引用关系,确保程序中各个模块之间的符号(如变量和函数)能够精确毗连和解析。
5.1.2 链接的作用
链接的核心作用是将多个对象文件和库文件中的代码和数据精确地毗连在一起,办理它们之间的引用问题,并将程序中的外部符号(例如函数和变量)绑定到精确的地址上。具体作用如下:
5.1.2.1 符号解析
符号解析是链接的一个重要使命。一个程序中可能包罗多个源文件,这些源文件在编译时生成多个对象文件。在编译过程中,每个对象文件会生成一个符号表,记录其中的函数、变量等符号。链接的过程就是将这些符号毗连起来,办理它们之间的引用。
5.1.2.2 地址分配
在链接过程中,链接器为每个符号(如函数、变量)分配一个虚拟地址。链接器需要根据程序的布局、符号的序次等因素,确定每个符号的内存地址。这一过程称为地址分配。
5.1.2.3 重定位
重定位是链接器根据程序的符号表和重定位信息,对对象文件中的地址进行调解的过程。程序中的某些地址(如函数和变量的地址)在编译时并不确定,链接器会根据符号解析的效果对这些地址进行修正。
5.1.2.4 库的链接
链接器还需要处理程序与库之间的链接。程序中引用的外部函数和变量通常界说在库文件中,链接器需要将程序的引用与库中的界说进行绑定。
5.1.2.5 生成可执行文件
链接器最终会生成一个可执行文件,这个文件是程序的最终输出,可以直接由操纵系统加载并运行。
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
https://i-blog.csdnimg.cn/direct/b3a26c22ca6d4e0f93e43af4eefcb15e.png
图5-1 链接过程
5.3 可执行目的文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的根本信息,包括各段的起始地址,大小等信息。
https://i-blog.csdnimg.cn/direct/8ed5698b15204adb841302e95f7a9908.png
图5-2 典范的ELF可执行目的文件
首先获得hello的ELF文件。
https://i-blog.csdnimg.cn/direct/90cb900a3be843b680355752ae3b292b.png
图5-3
5.3.1 ELF头
https://i-blog.csdnimg.cn/direct/7b272b41beb44e00bf26e15c725ad5c7.png
图5-3 ELF头
从ELF头可以看到:数据为小端序,文件范例为可执行文件,入口点地址为0x4010f0,程序头出发点为64,节头开始处为14208,ELF头的大小为64字节,程序头的大小为56字节,程序头的数量为12,节头大小为64比特,节头数量为27。
5.3.2 节头
https://i-blog.csdnimg.cn/direct/e9d2a53827b84bef8e7524795766a065.png
https://i-blog.csdnimg.cn/direct/6cc0ada483c44c9c8084b8a8d0c9c897.png
图5-4 节头
节头包罗差异节的名称、范例、地址、偏移量、大小等信息。由此可以得到各段的起始地址,大小等信息。
5.3.3 程序头
https://i-blog.csdnimg.cn/direct/45318532f3a9441283dcfdca06ba7004.png
图5-5 程序头
程序头形貌了可执行文件的连续的片到连续的内存段的映射。
5.3.4 段节
https://i-blog.csdnimg.cn/direct/58c13ea419a949528623f5b385d16601.png
图5-6 段节
5.3.5 .rela.dyn
https://i-blog.csdnimg.cn/direct/4374d105886a46ba9958fc303134ed0d.png
图5-7 .rela.dyn
.rela.dyn是一个重定位节,通常用于存储与.dyn节的重定位相干的数据。当目的文件与其他文件组合时,它告诉链接器需要怎样调解.dyn节中某些位置的指令或数据,确保程序在运行时能够精确调用函数、访问变量等。
5.3.6 .rela.plt
https://i-blog.csdnimg.cn/direct/99fa048c05e74170ab5b1e8f14d8adf0.png
图5-8 .rela.plt
.rela.plt是一个重定位节,通常用于存储与.plt节的重定位相干的数据。
5.3.7 .dynsym
https://i-blog.csdnimg.cn/direct/3c72e78ff7ec454fb2dbf52ede971681.png
图5-9 .dynsym
5.3.8 .symtab
https://i-blog.csdnimg.cn/direct/6c58143cab184e77a5f10e4e2dac46ec.png
图5-10 .symtab
在ELF文件格式中,.symtab节是一个非常重要的部分,它存储了程序中的符号信息,符号通常代表程序中的变量、函数、对象等。
5.4 hello的虚拟地址空间
利用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析阐明。
https://i-blog.csdnimg.cn/direct/532e3222d74e4e42a69db6a845e08952.png
图5-11 Date Dump
5.5 链接的重定位过程分析
首先获得hello的反汇编文件。
https://i-blog.csdnimg.cn/direct/c878f811966c468f8a7d374921cb1995.png
图5-12 hello的反汇编过程
5.5.1 hello与hello.o的差异
与2.asm相比,4.asm多了_init, .plt, puts@plt, printf@plt, getchar@plt等部分。因为程序中引用了这些外部函数,在链接的过程中,链接器需要将程序的引用与库中的界说进行绑定,从而生成可执行文件hello,因此在4.asm中能看到这些部分。
5.5.2 hello中对hello.o的重定位
5.5.2.1 符号解析与重定位
hello.o中调用了外部函数,如printf,编译器在生成hello.o文件时不知道printf函数的具体地址。因此,链接器将printf作为一个外部符号记录在符号表中,并在hello.o的重定位表中插入相干重定位信息。在重定位过程中,链接器会查找printf符号的界说地址,并将hello.o文件中的全部printf调用地址调解为实际的库函数地址。
5.5.2.2 符号地址的调解
对于hello.o中界说的符号(如局部变量或函数),链接器会根据符号表为它们分配内存地址。假设hello.o中有一个变量x,在编译时,链接器将x的地址设置为一个占位符。链接器会根据最终的地址分配效果调解该占位符,确保它指向精确的内存位置。
5.5.2.3 代码段和数据段的重定位
代码段(.text)和数据段(.data)中存储的是程序的指令和数据,但这些部分的地址在编译时是相对的或未知的。在链接时,链接器会根据程序最终的内存布局调解这些地址,确保程序能够精确访问它们。
5.5.2.4 符号表和重定位表的更新
符号表中的符号会根据链接器的地址分配进行更新。例如,函数main和printf的地址会在链接时被解析和修改。重定位表则会记录这些符号在地址调解后的偏移量。
5.6 hello的执行流程
_start
_init
main
printf@plt
exit@plt
printf@plt
atoi@plt
sleep@plt
getchar@plt
_fini
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变革。要截图标识阐明。
5.8 本章小结
链接是将源代码编译后的对象文件(.o文件)和库文件(如静态库、动态库)组合成最终可执行文件或共享库的过程。通过查看hello的ELF文件,分析链接的重定位过程和hello的执行流程,链接对我来说不再是那么神秘了。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程是指执行中的程序的实例。它是系统资源分配的根本单位,是操纵系统调度的根本单位。进程不仅仅包括程序代码,还包括与程序执行相干的全部资源,如内存、文件、输入输出设备、寄存器等。
6.1.2 进程的作用
6.1.2.1 程序执行的载体
进程是程序的实际执行单元。一个程序在执行时,操纵系统会为其创建一个进程,并为该进程分配系统资源(如内存、CPU时间、I/O设备等)。进程为程序的执行提供了必须的资源和情况,使得程序能够独立运行。
6.1.2.2 资源管理与隔离
操纵系统通过进程实现对计算机资源的管理和隔离。每个进程拥有独立的地址空间,差异进程之间的内存和资源相互隔离,确保了进程之间不会直接相互干扰。操纵系统通过进程控制来管理计算机的CPU、内存、I/O设备等资源,确保资源的公道分配与回收。
6.1.2.3 并发执行
通过进程,操纵系统可以实现多个程序的并发执行。在多核处理器的情况下,操纵系统可以通过多进程或多线程的方式同时运行多个进程,实现使命的并发处理。进程的并发执行使得计算机能够更高效地利用 CPU 资源,提拔系统的团体性能。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 作用:
6.2.1.1 命令解释器
Shell作为命令解释器,负责解析用户输入的命令并将其通报给操纵系统内核执行。它处理命令的语法、参数、选项等,确保精确执行。
6.2.1.2 命令行界面
Shell提供了一个文本界面,答应用户通过键盘输入命令并接收操纵系统返回的输出。这个界面通常用于系统管理、程序开辟、调试、自动化脚本等操纵。
6.2.1.3 脚本执行器
Shell还可以用来执行脚本。用户可以编写Shell脚本,将一系列命令、控制布局(如循环、条件判定)和变量整合在一起,通过一个命令执行多项使命。
6.2.1.4 进程控制
Shell可以启动、管理和停止进程。例如,通过ps命令查看进程,通过kill命令停止进程。
6.2.1.5 管道与重定向
Shell提供了管道和重定向功能,答应用户将多个命令的输出和输入进行毗连或重定向到文件。这使得Shell成为非常强盛的工具,可以将多个小程序组合成一个复杂的命令序列。
6.2.2 处理流程:
6.2.2.1. Shell将用户输入的命令分割成命令名和参数。
6.2.2.2. Shell根据情况变量PATH查找命令的实际路径。
6.2.2.3. Shell处理命令中的通配符,情况变量替换,命令替换等。
6.2.2.4. Shell将解析后的命令交给操纵系统的内核执行,操纵系统会根据命令范例进行相应的操纵。
6.2.2.5. 假如是内置命令,不需要创建新进程,Shell自身处理。
6.2.2.6. 假如是外部命令,Shell会创建一个新进程来执行该命令。
6.2.2.7. 命令执行完毕后,操纵系统返回执行效果,Shell将其表现在屏幕上,供用户查看。
6.2.2.8. Shell会表现新的提示符,等候用户输入下一条命令。
6.3 Hello的fork进程创建过程
[*]输入./hello 2023110151 孙灿阳 15856587696 1。
[*]该命令为外部命令,Shell会创建一个新进程来执行该命令。
[*]调用fork函数创建一个新的运行的子进程。
[*]子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件形貌符相同的副本,子进程可以读写父进程中打开的任何文件。
[*]命令在子进程执行完毕后,操纵系统返回执行效果,Shell将其表现在屏幕上。
6.4 Hello的execve过程
调用execve函数,操纵系统首先根据filename指定的路径和名称找到对应的可执行文件。然后,操纵系统创建一个新的进程,并将该可执行文件加载到新进程的内存空间中。接下来,操纵系统将新进程的参数和情况变量设置为argv和envp指定的内容。末了,操纵系统启动新进程的执行,开始执行hello。
6.5 Hello的进程执行
联合进程上下文信息、进程时间片,论述进程调度的过程,用户态与核心态转换等等。
6.5.1 上下文切换
上下文切换是操纵系统中进程调度的核心部分,它涉及到保存当前进程的状态,并加载下一个进程。这通常发生在以下几种情况:
时间片用完:操纵系统需要保存当前进程的上下文(寄存器、程序计数器等),并调度另一个进程。
进程阻塞:例如,进程调用sleep()进入阻塞状态,操纵系统会保存当前进程的上下文,调度其他进程运行。
在本程序中,固然每次sleep()后会有一个时间间隔,但实际上,程序只有在sleep()函数内部的时间等候时才会进行上下文切换。否则,每次输出后,进程的调度会依靠于操纵系统的时间片机制。
6.5.2 进程时间片
在现代操纵系统中,CPU时间被分配给进程是通过时间片来实现的。每个进程在获得CPU资源时,操纵系统会给它一个固定的时间片。时间片过后,操纵系统会进行上下文切换,将CPU资源分配给另一个进程。
在本程序中,时间片的概念可以在以下两种场景中体现:
正常的CPU调度:在每次printf()输出信息时,进程运行在用户态,直到它调用sleep()。此时,操纵系统可能会切换到其他进程(假如有其他进程在等候CPU资源)。
就寝期间的调度:当程序调用sleep()函数时,进程进入就寝状态,不占用CPU资源,操纵系统会调度其他停当的进程执行。
6.5.3 用户态与核心态转换
操纵系统将进程的执行分为两种状态:
用户态:程序在用户空间内运行,操纵系统不干预进程的执行,进程只能访问用户空间的内存,执行平凡的计算使命。
核心态:进程执行涉及操纵系统核心的操纵时,会进入核心态。例如,程序调用sleep()或其他系统调用时,操纵系统会切换到核心态来处理该哀求。
在hello的进程执行过程中,会进行用户态和核心态转换:
用户态到核心态的转换:当程序调用sleep()函数时,操纵系统会把当前进程挂起,进入核心态行止理系统调用。具体来说,sleep()通过系统调用进入核心态,操纵系统会把当前进程标志为“就寝”状态,并在指定的时间过后重新调度该进程。
核心态到用户态的转换:当sleep()调用竣事后,操纵系统会把进程规复到用户态,然后继承执行后续的程序(即printf)。
6.5.4 进程调度
hello的执行过程中,进程调度重要体如今以下几个方面:
进程在用户态执行程序(如printf()),然后通过核心态的系统调用(如sleep())将进程挂起。
操纵系统通过时间片机制调度进程,包管CPU资源的公道分配。
系统调用(如sleep())触发用户态与核心态的转换,从而实现进程的挂起和规复。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不绝乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,阐明异常与信号的处理。
6.6.1 hello执行过程中会出现的异常种类
中断、陷阱、故障和停止。
6.6.2处理方法
6.2.2.1中断的处理方法:
在当前指令完成执行之后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用得当的中断处理程序。当处理程序返回时,它就将控制返回给下一条指令(也即假如没有发生中断,在控制流中会在当前指令之后的那条指令)。效果是程序继承执行,就好像没有发生过中断一样。
6.2.2.2 陷阱的处理方法:
陷阱是故意的异常,是执行一条指令的效果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。
6.2.2.3 故障的处理方法:
故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。假如处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会停止引起故障的应用程序。
6.2.2.4 停止的处理方法:
停止是不可规复的致命错误造成的效果,通常是一些硬件错误,比如DRAM或者SRAM位被破坏时发生的奇偶错误。停止处理程序从不将控制返回给应用程序。处理程序将控制返回给一个abort例程,该例程会停止这个应用程序。
6.2.3 会产生信号的种类:
https://i-blog.csdnimg.cn/direct/da1296193f8449adac1d02f2bb138a31.png
图6-1 Linux信号
6.2.4 处理方法:
signal函数可以通过下列三种方法之一来改变和信号signum相干联的举动:
假如handler是SIG_IGN,那么忽略范例为signum的信号。
假如handler是SIG_DFL,那么范例为signum的信号举动规复为默认举动。
否则,handler就是用户界说的函数的地址,这个函数被称为信号处理程序,只要进程接收到一个范例为signum的信号,就会调用这个程序。通过把处理程序的地址通报到signal函数从而改变默认举动,这叫做设置信号处理程序。调用信号处理程序被称为捕获信号。执行信号处理程序被称为处理信号。
6.2.5 不绝乱按
https://i-blog.csdnimg.cn/direct/547cd9470c7c4c3a8f30c5e17595b5f6.png
图6-2 不绝乱按
乱按的内容会被输出,程序继承执行。
6.2.6 回车
https://i-blog.csdnimg.cn/direct/6c9fd4de81274538bab3f5fa6ac5d6e7.png
图6-3 回车
回车会出现空行,没有其他影响。
6.2.7 Ctrl-Z
https://i-blog.csdnimg.cn/direct/3907327880d245d2beff0d76aead83b6.png
图6-4 Ctrl-Z
进程收到信号,被挂起,并打印信息。
6.2.8 Ctrl-C
https://i-blog.csdnimg.cn/direct/27052b00f2c1402b80c4d7df8122fa44.png
图6-5 Ctrl-C
进程收到信号,停止。
6.2.9 Ctrl-z后运行ps命令
https://i-blog.csdnimg.cn/direct/ec520cc50ada44ba9cba39796db153a4.png
图6-6 Ctrl-z后运行ps命令
会打印各进程的PID等信息。
6.2.10 Ctrl-z后运行jobs命令
https://i-blog.csdnimg.cn/direct/6b14cf0285764d7e9f525d9f186a12d2.png
图6-7 Ctrl-z后运行jobs命令
会打印进程的状态。
6.2.11 Ctrl-z后运行pstree命令
https://i-blog.csdnimg.cn/direct/8cf3b8a96c1445069e289c70bc39cb68.png
图6-8 Ctrl-z后运行pstree命令
会打印进程的树状图。
6.2.12 Ctrl-z后运行fg命令
https://i-blog.csdnimg.cn/direct/a15739571ebe442d8ed66a3598e88e4c.png
图6-9 Ctrl-z后运行fg命令
会将hello重新调到前台执行,且总共的打印次数保持稳定。
6.2.13 Ctrl-z后运行kill命令
https://i-blog.csdnimg.cn/direct/7572c8c32ccf431a8d7b5e13fad0b4a0.png
图6-10 Ctrl-z后运行kill命令
进程收到信号,会被杀死。
6.7本章小结
本章围绕hello的进程管理展开。进程是指执行中的程序的实例,具有非常重要的作用。通过查阅相干的资料,我相识了壳Shell-bash的作用与处理流程、Hello的fork进程创建过程、Hello的execve过程以及Hello的进程执行。末了,通过具体实验,真真切切的看到了hello的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
联合hello阐明逻辑地址、线性地址、虚拟地址、物理地址的概念。
[*]逻辑地址
逻辑地址通常指的是由程序生成并利用的地址。程序中的变量、指针等都是通过逻辑地址来访问内存的。在程序执行过程中,CPU会生成逻辑地址,而这些地址实际上是通过操纵系统映射到物理内存的。
在hello的执行过程中,变量如argc、argv[]和i等,都是程序内部利用的,当你利用argv、argv等访问命令行参数时,程序实际上是通过逻辑地址来访问参数的内存空间。
[*]线性地址
线性地址是操纵系统对虚拟地址空间的一种映射情势。在一些计算机体系布局中,从虚拟地址到物理地址可能要经过多个转换,其中线性地址是虚拟地址转换的一个中间效果。
在现代操纵系统中,虚拟地址通过段页式管理转换为线性地址,之后再通过页表映射为物理地址。
[*]虚拟地址
虚拟地址是操纵系统为每个进程提供的一种抽象的内存空间。每个程序运行时,它会被分配一个虚拟地址空间,这个地址空间是由操纵系统和硬件协作来管理的。虚拟地址空间对于每个进程来说都是独立的,程序无需关心物理内存的具体位置。
在hello的执行过程中,当CPU执行printf、sleep等函数时,它会访问虚拟地址。虚拟地址由操纵系统通过地址转换机制映射到物理内存地址。
例如,程序在访问argv时,实际上是访问某个虚拟地址,操纵系统会将这个虚拟地址转换为对应的物理地址。
[*]物理地址
物理地址是计算机内存中实际存在的位置,是计算机硬件(RAM)中内存单元的实际地址。程序运行时,虚拟地址最终会被操纵系统转换为物理地址,指向物理内存中的某个具体位置。
在hello的执行过程中,固然利用的是虚拟地址,但操纵系统通过内存管理单元(MMU)将这些虚拟地址映射到物理内存上。例如,当程序访问argv[]数组时,操纵系统通过映射将这些虚拟地址转换为物理地址,然后CPU才能从内存中读取数据。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在Intel架构的x86处理器中,逻辑地址到线性地址的变换是通过段式管理机制完成的。在段式管理下,处理器利用逻辑地址来访问内存,而逻辑地址通常包罗段选择符和偏移量。这些部分需要通过段形貌符表转换成一个实际的线性地址。线性地址是操纵系统通太过页等技术进一步映射到物理内存的地址。
7.2.1 逻辑地址组成
在Intel的x86架构中,逻辑地址是由两部分组成的:
段选择符:16位的值,用于从全局形貌符表或局部形貌符表中查找段形貌符。
偏移量:32位(或更长,取决于模式),指定在该段内的数据位置。
7.2.2 段选择符
段选择符实际上是一个指向段形貌符的索引。段选择符的布局如下:
索引:13位,指向段形貌符表中的条目。
TI(Table Indicator):1位,指定段形貌符表的位置。假如TI=0,表现利用GDT;假如TI=1,表现利用LDT。
RPL(Request Privilege Level):2位,指定哀求的特权级(0-3),通常与访问控制相干。
段选择符指向一个段形貌符,段形貌符是存储在GDT或LDT中的一个数据布局,包罗了段的起始地址、段的大小、段的范例等信息。
7.2.3 段形貌符
段形貌符是一个8字节的数据布局,包罗了有关段的详细信息。最重要的字段包括:
基地址:段的起始地址。
段界限:段的大小或最大偏移量。
段范例、权限:段的权限和特性(如可执行、读写等)。
段大小标志:区分是否为利用字节为单位或页为单位的地址。
7.2.4 从逻辑地址到线性地址的转换过程
当程序访问一个逻辑地址时,CPU会通过段选择符获取段形貌符,然后将其与偏移量联合起来计算线性地址。这个转换过程可以分为以下几个步调:
步调1:查找段形貌符
CPU利用段选择符中的索引字段在GDT或LDT中查找相应的段形貌符。段形貌符包罗段的基地址和其他形貌信息。假如段选择符的TI位为0,段形貌符在GDT中查找。假如TI位为1,段形貌符在LDT中查找。
步调2:计算线性地址
一旦找到段形貌符,CPU就可以从中提取出段的基地址。段形貌符还包罗段的界限,即该段的最大偏移量。线性地址的计算方法为:线性地址 = 段基地址 + 偏移量。段基地址是从段形貌符中获得的基地址,偏移量是逻辑地址中的偏移量部分。
步调3:访问线性地址
线性地址是经过段管理系统转换后的地址。这个地址会被操纵系统进一步映射到物理内存地址(假如启用了分页机制的话),但在段式管理中,线性地址通常已经充足进行内存访问。
7.3 Hello的线性地址到物理地址的变换-页式管理
7.3.1 页式管理简介
页式管理是一种内存管理方案,将虚拟地址空间(包括线性地址)分成固定大小的块,称为页,以及对应的页框。操纵系统通过页表来将虚拟地址映射到物理地址。
7.3.2 线性地址的布局
在分页管理中,线性地址通常由多个部分组成。例如,在32位x86架构中,线性地址通常是32位的,分为三个部分:
页目次索引:位于线性地址的高10位。
页表索引:位于中间的10位。
页内偏移:位于低12位。
7.3.3 线性地址到物理地址的转换过程
当程序执行时,CPU会生成一个线性地址,该地址会通太过页机制进一步转换为物理地址。这个转换过程通常包括以下步调:
步调1:获取线性地址
首先,程序中的地址从逻辑地址经过段管理转换为线性地址。程序完成了段管理的步调后,我们就有了一个线性地址。例如,当你通过命令行传入参数时,argv[]存储在某个内存区域,你通过argv、argv、argv等访问这些参数。
步调2 查找页目次和页表
页目次:线性地址的高10位用于索引页目次。页目次表是一个包罗多个页表地址的表,每个条目指向一个页表。
页表:页表将线性地址的中间10位用于索引该页的具体条目,每个页表条目指向一个实际的物理页框。
步调3 获取物理地址
最终,页表条目会提供物理页框的基地址,联合页内偏移就可以计算出物理地址。物理地址的计算方法为:物理地址 = 物理页框基地址 + 页内偏移。
7.3.4 以hello为例
程序中的字符串argv、argv、argv被存储在内存中的某个位置。操纵系统通过段式管理将这些变量的地址映射为线性地址。
程序在循环中访问argv、argv、argv,这些操纵会利用线性地址。例如,当访问argv时,CPU利用该地址执行相应的内存访问操纵。
操纵系统的内存管理单元(MMU)将argv对应的线性地址转换为物理地址。转换过程如下:
首先,线性地址会被分解为:页目次索引、页表索引和页内偏移。
MMU利用页目次索引查找页目次表,找到相应的页表地址。
然后,MMU利用页表索引查找具体的页框地址,找到物理内存中的一个页框。
末了,MMU将页框基地址和页内偏移联合,得到物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
7.4.1 四级页表支持下的虚拟地址到物理地址的转换
在x86-64架构中,虚拟地址的大小通常是64位,但实际支持的虚拟地址空间较小(比如48位)。虚拟地址空间通过多级页表来进行管理,具体地,Intel的64位架构利用了四级页表来处理虚拟地址到物理地址的转换。
7.4.1.1 四级页表的布局
虚拟地址的布局为:
PML4(Page Map Level 4):高9位(第47位到第39位)用于索引到PML4表,即页目次指针表。
PD(Page Directory):紧接着的9位(第38位到第30位)用于索引到页目次。
PT(Page Table):接下来的9位(第29位到第21位)用于索引到页表。
Page Offset:低12位(第20位到第0位)表现页内偏移,指向物理页内的具体位置。
7.4.1.2 转换过程
PML4查找:首先利用虚拟地址的高9位在PML4表中查找对应的页目次指针表条目。假如找到该条目,该条目指向页目次的物理地址。
页目次查找:接下来,利用虚拟地址中的第38到第30位在页目次表中查找对应的页表条目。假如该条目有效,它指向页表的物理地址。
页表查找:然后,利用虚拟地址中的第29到第21位在页表中查找对应的页框条目。假如该条目有效,它指向物理内存中的一个页框。
页内偏移:末了,利用虚拟地址中的低12位作为页内偏移,来确定在该页框内的具体位置。
7.4.2 TLB
TLB(Translation Lookaside Buffer)是一个高速缓存,用于缓存最近的虚拟地址到物理地址的转换效果,目的是加速地址转换过程。由于虚拟地址到物理地址的转换可能涉及多次查找页表(尤其是在多级页表的体系布局下),每次查找都可能斲丧较多的时间。因此,TLB缓存了之前已查找到的虚拟地址到物理地址的映射,减少了每次转换时的查找时间。
7.4.2.1 TLB的工作原理
当CPU需要访问某个虚拟地址时,它首先会查抄TLB是否有该虚拟地址的映射。假如TLB中有,这时我们称为TLB掷中,CPU直接从TLB中获得物理地址,访问内存。假如TLB中没有,这时我们称为TLB未掷中。CPU需要进行页表查找,并将新的虚拟地址到物理地址的映射加载到TLB中,以便下次快速访问。
7.4.2.2 TLB的设计
大小:TLB通常比页表要小得多,但它黑白常快速的。它可能只有几百个条目,且它通常接纳全相联或组相联的设计方式。
替换战略:当TLB已满时,会利用某种替换战略(如LRU最少利用算法)来淘汰旧的条目,腾出空间缓存新的映射。
7.4.2.3 TLB中的条目
每个TLB条目通常包括:
虚拟页号(VPN):虚拟地址中的页目次、页表和页框的组合,表现虚拟页。
物理页号(PPN):映射到该虚拟页的物理页号。
控制信息:如有效位、权限位等。
7.4.3 总结
在现代x86-64架构下,虚拟地址到物理地址的转换通过四级页表和TLB缓存机制共同工作:
四级页表:虚拟地址通过四级页表(PML4 -> 页目次 -> 页表 -> 页内偏移)进行转换,最终得到物理地址。
TLB缓存:TLB缓存了常用的虚拟地址到物理地址的映射,从而避免了频繁的多级页表查找,进步了内存访问的效率。
这种多级页表加上TLB缓存的方式,使得现代计算机可以高效、机动地管理大规模的虚拟内存,同时提供了更好的性能和内存保护。
7.5 三级Cache支持下的物理内存访问
7.5.1 三级Cache简介
在现代处理器中,通常会有三级缓存(L1、L2、L3),它们的重要作用是缓存访问频繁的数据,以减少访问主存的延迟。每个级别的缓存都有其特点:
L1 Cache:通常是最小且最快的缓存,通常分为数据缓存和指令缓存。L1 Cache一般位于处理器核心内部,访问延迟最小,容量较小(一般为32KB到128KB)。
L2 Cache:相较于L1,L2 Cache较大,访问速度较慢,但依然比物理内存快得多。L2 Cache通常与处理器核心相干联,每个核心有独立的L2 Cache,或多个核心共享。
L3 Cache:L3 Cache是级别最高的缓存,通常为全部处理器核心共享,容量更大(几MB到十几MB不等),但访问速度较慢。L3 Cache的重要作用是进一步减少访问主存的频率。
7.5.2 物理内存访问的过程
处理器执行指令时,涉及从差异条理的缓存或主存中读取数据。这个过程的目的是尽量减少访问物理内存的次数,而优先从高速缓存(L1、L2、L3)中获取数据。
物理内存访问过程包括以下几个步调:
步调1:查找L1缓存
当处理器需要读取数据时,首先查抄L1数据缓存是否包罗所需的数据。假如缓存掷中(即L1 Cache中有该数据),处理器可以直接从L1 Cache中获取数据,访问延迟最低。
L1缓存掷中,CPU直接从L1 Cache获取数据;L1 Cache未掷中,处理器将哀求L2 Cache。
步调2:查找L2 Cache
假如 L1 Cache未掷中,处理器会继承查抄 L2 Cache。L2 Cache通常较大,存储的数据比 L1 Cache更多,通常会包括最近利用但不频繁访问的数据。
假如数据存在于 L2 Cache,处理器会将数据从 L2 Cache读取,然后更新 L1 Cache;假如数据在 L2 Cache中也没有,处理器将哀求 L3 Cache(假如有)或者 主内存。
步调3:查找 L3 Cache
L3 Cache较大,通常是多核处理器共享的缓存。在 L3 Cache中查找数据比从主内存读取要快得多。
假如数据存在于 L3 Cache,处理器将数据读取到 L2 Cache,并可能将其通报到 L1 Cache,以便更快的访问;假如数据在 L3 Cache中也没有,处理器将从主存中读取数据。
步调4:访问主存
假如数据不在L1、L2或L3 Cache中,末了的步调是从物理内存(通常是DRAM)中读取数据。由于访问物理内存的速度远远低于访问缓存,处理器的访问延迟会显著增长。
处理器通过内存控制器与主内存交互,读取数据后,通常会将数据加载到L3 Cache,L2 Cache,甚至L1 Cache中,以便后续的快速访问。
7.5.3 物理内存访问过程中的缓存战略
7.5.3.1 缓存替换战略:
当缓存已满时,需要决定将哪个数据移出缓存。常见的缓存替换战略包括:
LRU:将最长时间未被访问的数据替换出缓存。
FIFO:将最早进入缓存的数据替换出缓存。
随机替换:随机选择一个缓存项进行替换。
7.5.3.2 写战略
处理器写数据时,有两种常见的写战略:
写直达:数据同时写入缓存和主内存。这样可以确保缓存与内存一致,但写操纵的延迟较高。
写回:数据先写入缓存,只有当缓存中的数据被替换时,才会将修改的内容写回主内存。写回战略可以减少内存写操纵的次数,但会增长缓存一致性管理的复杂性。
7.5.3.3 缓存一致性
在多核处理器中,各个核心有可能有本身的本地缓存(L1和L2),但多个核心间共享L3 Cache。为了保持缓存中的数据一致性,通常会接纳缓存一致性协议,如MESI协议,来确保各个缓存中的数据在读取和写入时的一致性。
7.6 hello进程fork时的内存映射
7.6.1 进程地址空间
每个进程都有本身的虚拟地址空间。虚拟地址空间大抵可以分为以下几个部分:
代码段:存放程序的机器码(代码)。
数据段:存放已初始化的全局变量和静态变量。
BSS段:存放未初始化的全局变量和静态变量。
堆:存放动态分配的内存(例如通过malloc和free管理的内存)。
栈:存放函数调用时的局部变量和函数调用信息。
7.6.2 fork()的举动
当调用fork()时,操纵系统会为子进程复制父进程的地址空间。
传统上,父进程和子进程在调用fork()后会有完全相同的内存映射,子进程将获得父进程内存的完整副本。但在现代操纵系统中,通常会接纳“写时复制”战略来优化fork()的性能。这意味着,直到父进程或子进程实验修改某个内存页时,操纵系统才会为该页创建一个副本。换句话说,父进程和子进程在fork()后共享相同的内存,只有在修改时才会各自独立。
7.6.3 内存映射的具体变革
在调用fork()后,父子进程的内存映射会发生以下几种变革:
代码段:由于代码段是只读的,因此父进程和子进程可以共享同一段内存,不需要复制。
数据段和BSS段:这些区域通常包罗全局变量和静态变量,父进程和子进程会各自有一个副本。
堆:对于堆内存,父进程和子进程最初会共享相同的内存页,但它们的虚拟地址差异,修改堆中的数据时会触发COW举动,从而创建新的内存页。
栈:栈内存对于父进程和子进程是独立的,因为栈是与函数调用和局部变量相干的。
7.7 hello进程execve时的内存映射
7.7.1 execve()简介
execve()的作用是将当前进程的内存空间完全替换为新的程序(包括代码、数据、堆、栈等)。执行execve()后,当前进程的执行状态、内存和代码都会被新的程序所替换。execve()是一个非常强盛的系统调用,通常用于启动新的程序。
int execve(const char *pathname, char *const argv[], char *const envp[]);
pathname:要执行的新程序的路径。argv:新的程序的命令行参数。envp:新的程序的情况变量。
7.7.2 execve()的内存映射变革
当调用execve()时,操纵系统会将当前进程的地址空间完全扫除,并将新的程序加载到进程的地址空间中。以下是内存映射的具体变革:
代码段:新的程序的代码段会被加载到进程的内存中,替换原先的代码段。当前进程的代码段被扫除。
数据段和BSS段:新的程序的全局变量和静态变量会加载到数据段中,原进程中的这些变量被抛弃。BSS段会被初始化为零。
堆:新的程序的堆会被初始化,原先的堆内存会被开释,新的程序从其起始位置开始利用堆内存。
栈:新的程序会重新设置栈,栈的内容也会被重置。
7.8 缺页故障与缺页中断处理
7.8.1 缺页故障简介
缺页故障是指程序访问虚拟内存中的某个地址时,该地址所对应的页面不在物理内存中,导致一个异常变乱。虚拟内存的关键机制就是程序可以访问一个非常大的虚拟地址空间,而不必将全部的内存都装载到物理内存中。当程序访问一个尚未加载到物理内存的虚拟页面时,就会触发缺页故障。
7.8.2 缺页故障的缘故原由
缺页故障通常发生在以下几种情况下:
页面不在物理内存中:当程序访问的虚拟地址所在的页没有加载到物理内存时,会产生缺页故障。
懒惰加载:虚拟内存的页面可能在程序运行时才被加载,即只有在首次访问时,操纵系统才将其加载到物理内存中。此时访问一个尚未加载的页面时会发生缺页故障。
页面被换出:操纵系统利用页面置换算法(如LRU、FIFO等)将一些页面从物理内存中换出到磁盘。假如程序后续访问这些被换出的页面,缺页故障将再次发生。
7.8.3 缺页中断处理流程
当缺页故障发生时,操纵系统需要通过一系列步调来处理这一中断。以下是典范的缺页中断处理流程:
7.8.3.1 硬件触发缺页中断
当程序访问的虚拟页面不在物理内存中时,硬件会触发一个缺页中断。处理器会保存当前的程序状态,并将控制权交给操纵系统的缺页中断处理程序。
7.8.3.2 查抄缺页缘故原由
操纵系统的缺页中断处理程序会查抄页面是否有效。假如页面不存在,操纵系统需要从硬盘中读取该页面并将其加载到内存中。假如是内存保护错误(例如非法访问只读内存等),操纵系统可能会停止进程。
7.8.3.3 查找空闲物理页面
操纵系统需要在物理内存中找到一个空闲的页面来装载缺失的虚拟页面。假如物理内存中没有空闲的页面,操纵系统需要进行页面置换(即选择一个已加载的页面,先将其写回磁盘,然后加载新的页面)。
7.8.3.4 页面置换(假如需要)
假如没有空闲的物理页面,操纵系统利用页面置换算法(如LRU、FIFO、Clock等)选择一个页面将其换出。被换出的页面会被写回磁盘,以便腾出空间加载新的页面。
7.8.3.5 从磁盘加载页面
操纵系统将所需的虚拟页面从磁盘中加载到物理内存中。读取操纵通常会涉及磁盘I/O,这可能是一个较慢的过程。
7.8.3.6 更新页表
一旦页面被加载到内存,操纵系统会更新页表,将对应虚拟页面的页表项标志为有效,并设置精确的物理地址。
7.8.3.7 规复进程执行
操纵系统更新完页表后,规复进程的执行,程序可以继承执行原来的指令。此时,程序访问的虚拟地址已被映射到物理内存的实际页面,缺页故障处理完成。
7.8.4 缺页故障的性能影响
缺页故障的处理需要花费时间和系统资源,尤其是在缺页故障频繁发生的情况下,可能导致程序运行缓慢。缺页故障的处理过程通常会导致以下性能问题:
磁盘I/O延迟:从磁盘加载页面是一个相对较慢的操纵,特殊是与内存访问相比。频繁的磁盘I/O操纵会显著影响程序的执行效率。
页面置换开销:假如物理内存有限,频繁的页面置换可能会增长处理开销,尤其是在利用复杂页面置换算法时。
"页面抖动":当系统频繁地将页面换入换出时,可能出现所谓的“页面抖动”征象,进程的执行会因频繁的内存访问和磁盘I/O而极度缓慢。
7.8.5 减轻缺页故障负面影响的战略
为了减少缺页故障的发生和其带来的性能开销,操纵系统会接纳一些优化战略:
增长物理内存:增长系统中的物理内存可以减小缺页故障的发生率。
优化页面置换算法:选择高效的页面置换算法(如LRU、Clock算法等),确保最不常用的页面被优先换出。
局部性原理:程序通常具偶然间局部性和空间局部性,操纵系统会利用这些特性预加载页面或利用高效的预取战略来减少缺页故障。
内存映射文件:通过内存映射文件,可以避免频繁的磁盘I/O操纵,提拔性能。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的根本方法与战略。
7.9.1 内存分配
动态内存的分配通常是通过以下函数实现的:
malloc(size_t size):分配size字节的内存块。返回一个指向该内存块的指针。假如分配失败,返回 NULL。
calloc(size_t num, size_t size):分配一个包罗num个元素,每个元素size字节的内存块,并初始化为零。返回一个指向该内存块的指针。假如分配失败,返回 NULL。
realloc(void *ptr, size_t size):重新调解已经分配内存的大小。ptr是先前通过malloc或calloc分配的内存块的指针,size是新的内存大小。假如分配成功,返回一个指向新内存的指针。假如分配失败,返回NULL,且原内存保持稳定。
7.9.2 内存开释
动态内存的开释是通过free函数实现的:free(void *ptr):开释先前通过malloc、calloc或realloc分配的内存块。调用free后,指向该内存块的指针不再有效,应将其设为NULL以避免悬挂指针问题。
7.9.3 内存池
内存池是将多个相似大小的内存块会合在一起管理的一种技术。程序预先分配一块大的内存区域,然后通过自界说的分配器在这个内存池中分配和回收内存。
长处:减少频繁调用操纵系统的内存分配和回收函数的开销。
应用场景:例如,游戏中的对象池,或者线程池中的工作线程。
7.9.4 内存分配战略
动态内存分配的战略和方法许多,重要目的是有效地管理内存并避免内存碎片。以下是几种常见的战略:
7.9.4.1 首次适应
在内存块列表中,从头开始查找第一个充足大的空闲块来分配内存。
长处:简朴,执行速度较快。
缺点:可能会留下较小的碎片,导致内存碎片化。
7.9.4.2 最佳适应
查找全部空闲块中最适合哀求大小的块,即最小的可以容纳哀求的空闲块。
长处:减少内存碎片,尽可能使内存块更紧凑。
缺点:查找过程较慢,尤其是在大量小内存块的情况下,轻易导致大量的小碎片。
7.9.4.3 最差适应
查找最大的空闲块来进行内存分配,目的是盼望剩余的空闲块尽可能大,减少碎片化。
长处:避免将大块内存分配给小哀求,减少碎片。
缺点:可能会浪费空间,且管理较为复杂。
7.9.4.4 延迟分配
只有在实际利用内存时才进行内存分配,而不是提前分配。这样可以减少内存利用,并避免不须要的内存分配。
7.9.4.5 紧凑化
在内存中定期整理碎片,通过移动内存块来消除碎片,使内存更加紧凑。
长处:避免了长期存在的小碎片。
缺点:需要较大的开销,且可能会影响性能。
7.9.5 内存碎片
内存碎片是动态内存分配中的一个问题,指的是由于频繁的内存分配和开释,导致内存空间被切割成许多小块,不能有效利用。碎片通常分为两种:
外部碎片:程序中的空闲内存区域被分散在整个内存中,导致即使总的空闲内存充足,但无法满足一个大的内存哀求。
内部碎片:由于分配的内存块大于实际需要的内存量,导致剩余的内存不能被利用。
处理内存碎片的方法:
内存池:通过预分配大块内存并进行细粒度的分配,减少碎片的发生。
内存紧凑化:通过移动内存中的块,避免空洞的产生。
分区分配:将内存分为多个大小相同的区域,每个区域内管理一个内存块,这样可以减少碎片。
7.9.6 内存泄漏
内存泄漏发生在程序分配了动态内存之后,没有精确开释这部分内存,导致无法再访问这块内存。随着时间推移,内存泄漏会导致程序利用越来越多的内存,最终斲丧完系统的内存。
处理内存泄露的方法:
手动管理内存:程序员需要确保每次分配内存后,都调用free来开释内存。
智能指针:在C++中,智能指针(如std::unique_ptr和std::shared_ptr)可以自动管理内存,减少内存泄漏的风险。
内存泄漏检测工具:例如Valgrind、ASAN等工具可以帮助检测和修复内存泄漏问题。
7.9.7 Printf与动态内存管理
在C语言中,printf函数可能会间接调用动态内存分配函数,尤其是在格式化字符串或输出缓冲区的管理上。例如,printf会根据格式化字符串的内容动态分配缓冲区以存储输出内容。
7.10本章小结
这一章围绕hello的存储管理展开。通过查阅资料,我对hello的存储器地址空间、段式管理、页式管理、TLB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问、hello进程fork时的内存映射、hello进程execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理有了较为清晰的熟悉,学到了许多知识,也深深领会到了存储技术的妙处。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
在Linux操纵系统中,IO设备管理方法重要通过设备的模型化和统一的接口来进行。这种设计模式使得各种设备(如磁盘、网络接口、字符设备等)可以通过相同的方式进行访问和管理,从而简化了开辟和利用。
8.1.1 设备的模型化:文件
在Linux中,全部的设备都通过文件来进行表现。具体来说,设备被视为文件的一部分,而文件又是通过系统调用进行操纵的。这样的设计模式有助于统一管理和访问差异的硬件设备,并为用户提供一致的接口。
设备文件:在Linux中,设备通过特殊文件(通常在/dev目次下)来表现,这些文件被称为设备文件。设备文件分为两大类:字符设备:与字符流的设备进行交互,例如键盘、串口设备、打印机等。字符设备通过一次读取或写入一个字符的方式与设备交互。块设备:与块存储设备进行交互,例如硬盘、SSD等。块设备将数据分为多个块,可以进行随机读取和写入操纵。
文件抽象:设备文件在文件系统中表现为平凡文件,用户或应用程序可以通过标准的文件操纵接口(如open、read、write、ioctl等)来与设备交互。这种设计将硬件设备的操纵与常规的文件操纵联合起来,简化了程序的开辟和硬件的管理。
8.1.2 设备管理:Unix IO接口
Unix及其衍生操纵系统(如Linux)通过统一的IO接口来进行设备管理。IO接口使得用户可以通过文件操纵来访问差异种类的设备,而无需关心底层硬件的实现细节。重要的Unix IO接口包括以下几个部分:
打开设备:通过open()系统调用来打开设备文件。设备文件路径通常位于/dev目次下,例如,磁盘设备可能是/dev/sda,字符设备可能是/dev/ttyS0。
读取和写入:设备文件一旦被打开,用户就可以利用read()和write()函数进行数据的传输。例如,向磁盘设备写入数据或从网络设备读取数据。
控制设备:一些设备需要特殊的控制命令,这些命令通常通过ioctl()系统调用来发送。例如,修改串口设置、获取磁盘状态等操纵可以通过ioctl来完成。
关闭设备:完成设备的操纵后,可以通过close()来关闭设备文件,开释资源。
8.1.3 设备驱动模型
Linux操纵系统通过设备驱动程序来管理设备的实际操纵。设备驱动程序通常提供底层的硬件抽象,并通过系统调用接口与内核和用户空间进行交互。设备驱动程序负责将操纵系统的文件操纵转化为硬件设备的实际命令。
字符设备驱动:例如,对于一个串口设备,字符设备驱动将用户空间的read()和write()哀求转化为串口硬件的读写操纵。
块设备驱动:例如,对于磁盘设备,块设备驱动负责将read()和write()哀求映射到实际的磁盘块读取和写入操纵。
网络设备驱动:对于网络设备,网络设备驱动会将应用程序的发送和接收哀求转换为网络协议栈的操纵,涉及数据包的构造和传输。
8.1.4 设备模型的统一性与扩展性
Linux设备管理模型的一个重要特点是它的统一性和扩展性。通过将全部设备都建模为文件,Linux系统可以利用同一套接口进行管理和操纵,无论设备是硬盘、网络接口还是虚拟终端。别的,Linux系统的设备管理接口非常机动,可以方便地添加新范例的设备支持。
设备类:Linux设备管理将设备按范例划分为多个类别,如字符设备、块设备、网络设备等。每个类别有差异的处理机制和接口。
设备文件系统(devfs):设备文件系统是Linux中的一个虚拟文件系统,用于管理设备文件的创建和删除。随着现代Linux系统的发展,devfs逐渐被udev(用户空间设备管理器)取代,udev通过动态创建设备节点的方式进步了设备管理的机动性。
内核模块:许多设备驱动程序以内核模块的情势存在,这使得设备驱动可以在不重启系统的情况下加载和卸载。通过模块化的方式,Linux系统支持多种硬件设备和驱动的扩展。
8.2 简述Unix IO接口及其函数
Unix IO接口是操纵系统与用户空间程序之间进行数据交换的关键机制。在Unix系统中,险些全部的设备(包括文件、终端、网络设备等)都被视为文件,并通过标准的文件操纵接口进行访问。这种统一的接口提供了高效、简洁和机动的数据访问方式。
8.2.1 Unix IO接口简介
Unix IO接口提供了一套用于文件、设备和网络通讯的标准化函数,使得应用程序可以以雷同访问平凡文件的方式与硬件设备进行交互。常见的IO操纵包括打开、读取、写入、关闭文件等。
重要函数和系统调用:打开文件:open();读取文件:read();写入文件:write();关闭文件:close();文件状态控制:ioctl();文件定位:lseek();文件权限查抄:access()。
8.2.2 open()
open()系统调用用于打开文件或设备,并返回一个文件形貌符。文件形貌符是一个整数,用来表现打开的文件或设备。通过这个文件形貌符,应用程序可以进行后续的读写操纵。
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开的文件或设备的路径。flags: 打开文件时的标志。mode: 文件创建时的权限模式(仅在文件不存在时需要指定)。
返回值:成功时返回文件形貌符,失败时返回-1。
8.2.3 read()
read()系统调用用于从文件或设备中读取数据。
ssize_t read(int fd, void *buf, size_t count);
fd: 文件形貌符。buf: 用于存储读取数据的缓冲区。count: 要读取的字节数。
返回值:实际读取的字节数,若到达文件末端则返回0,失败时返回-1。
8.2.4 write()
write()系统调用用于向文件或设备写入数据。
ssize_t write(int fd, const void *buf, size_t count);
fd: 文件形貌符。buf: 存储写入数据的缓冲区。count: 要写入的字节数。
返回值:成功写入的字节数,失败时返回-1。
8.2.5 close()
close()系统调用用于关闭文件形貌符,开释相干资源。
int close(int fd);
fd: 要关闭的文件形貌符。
返回值:成功时返回0,失败时返回-1。
8.2.6 ioctl()
ioctl()系统调用用于设备控制,答应应用程序向设备驱动程序发送特殊的控制命令。这些命令通常与设备的特殊功能相干,如获取设备状态、设置设备参数等。
int ioctl(int fd, unsigned long request, ...);
fd: 文件形貌符。request: 哀求的控制命令。后续的参数:命令所需的附加参数。
返回值:成功时返回0,失败时返回-1。
8.2.7 lseek()
lseek()系统调用用于定位文件指针。通过这个函数,程序可以在文件中随机访问指定的位置。
off_t lseek(int fd, off_t offset, int whence);
fd: 文件形貌符。offset: 文件指针的偏移量。whence: 偏移量的起始位置。
返回值:成功时返回新的文件指针位置,失败时返回-1。
8.2.8 access()
access()系统调用用于查抄文件或目次的权限,通常用于程序执行前查抄文件是否可以访问。
int access(const char *pathname, int mode);
pathname: 要查抄的文件路径。mode: 权限范例。
返回值:成功时返回0,失败时返回-1。
8.3 printf的实现分析
8.3.1 printf函数的实现
printf是C语言中的标准输出函数,它用于将格式化的文本输出到终端或表现设备。在这个实现中,printf函数通过调用vsprintf来生成格式化的输出,然后利用write系统调用将内容输出到表现器。
https://i-blog.csdnimg.cn/direct/b11258c9810748b88241a2cfe645114b.png
图8-1 printf
va_list arg = (va_list)((char*)(&fmt) + 4):这行代码的目的是跳过格式字符串 fmt,获取反面可变参数的地址。因为在32位系统中,指针fmt占4个字节,以是通过+4跳过fmt,指向第一个可变参数。
vsprintf(buf, fmt, arg):这个函数负责将格式字符串和可变参数按照指定的格式生成最终的字符串,并将生成的字符串存储在buf中。
write(buf, i):利用write函数将格式化的字符串buf(长度为i)写入表现设备。write是通过系统调用来完成输出的,它将数据传输到屏幕或终端。
8.3.2 vsprintf函数的实现
vsprintf函数负责解析格式字符串fmt,根据格式阐明符(如%x)将可变参数格式化并填充到缓冲区buf中。
https://i-blog.csdnimg.cn/direct/5bd2bcde936e4c208ba5f710e5ee9638.png
图8-2 vsprintf
itoa(tmp, *((int*)p_next_arg)):将p_next_arg指向的整数转换为十六进制字符串并存储在tmp中。这里假设参数是int范例(占4字节)。
strcpy(p, tmp):将转换得到的字符串tmp复制到buf中。
p_next_arg += 4:每次处理一个整数参数后,p_next_arg会向后移动4字节,指向下一个参数。
(p - buf):返回写入buf的字符数。
8.3.3 write系统调用
write是一个系统调用,用于将数据输出到设备。这个实现中,write被用来将格式化后的字符串发送到表现设备(如表现器)。
https://i-blog.csdnimg.cn/direct/476d26ecaa5548bbbf3ca2753153d485.png
图8-3 write
mov eax, _NR_write:将系统调用号(write的系统调用号)放入eax寄存器。
mov ebx, :将文件形貌符(通常是1,表现标准输出)放入ebx寄存器。
mov ecx, :将缓冲区(要输出的字符串)的指针放入ecx寄存器。
int INT_VECTOR_SYS_CALL:利用int 0x80中断触发系统调用,将控制权转交给内核处理实际的I/O操纵。
8.3.4 表现输出的过程
最终,printf输出的内容将通过write系统调用通报到表现设备。整个过程如下:
8.3.4.1 字符映射
每个字符都被转换为一个位图(即字符的像素表现)。这些字符的位图数据通常保存在一个字符集(或字体库)中。
8.3.4.2 从字符到像素的转换
当生成格式化的字符串后,每个字符会根据其在字体库中的界说,转换为对应的像素矩阵(比如8x8或16x16点阵)。
8.3.4.3 写入显存(VRAM)
表现器的显存(Video RAM)存储了屏幕上每个像素的信息。生成的字符位图会被写入显存中,按精确的序次分列在屏幕上。
8.3.4.4 表现刷新
表现芯片(如LCD或CRT)会根据设定的刷新率(通常为60Hz)周期性地从显存中读取数据,并将其转换为电信号传输到表现屏,最终出现给用户。
8.4 getchar的实现分析
8.4.1 getchar()的实现
getchar()的根本作用是等候用户输入一个字符,并返回该字符的ASCII值。具体过程是,程序通过调用标准输入流的底层I/O函数来获取用户输入,当用户输入一个字符后按下回车键,getchar()将会读取到该字符并返回。
getchar()的扼要实现流程:
该函数首先调用系统底层的I/O函数来读取输入字符。
假如输入字符并不是回车键,它会将字符返回给程序。
假如按下回车键(通常是换行符 \n),则getchar()函数才会竣事当前输入并返回。
8.4.2 异步异常-键盘中断的处理
8.4.2.1 键盘输入的工作原理
硬件中断:当用户按下键盘上的某个键时,键盘生成一个中断信号,通知CPU进行处理。这是一个异步变乱,因为它是在程序的执行过程中独立触发的。
扫描码与ASCII码:每次按下一个键时,硬件首先生成一个扫描码,表现某个键的物理位置。操纵系统的键盘驱动程序将扫描码转换为对应的字符的ASCII码,然后将其存储在内核的缓冲区中。
缓冲区与系统调用:当程序调用getchar()等函数时,操纵系统通过其标准I/O库(如glibc)从键盘缓冲区读取数据。假如缓冲区中有数据,getchar()会立刻返回;否则,它会等候用户输入直到按下回车键。
8.4.2.2 具体操纵流程
键盘驱动程序:当键盘按键被按下时,键盘硬件会发出中断哀求,操纵系统的键盘驱动程序会响应此哀求。驱动程序将键的扫描码转换为相应的ASCII字符,并将字符放入一个输入缓冲区。
缓冲区管理:操纵系统会维护一个缓冲区(例如,终端或控制台的输入缓冲区),存储用户输入的字符。缓冲区通常接纳FIFO(先进先出)战略,确保字符按输入序次处理。
getchar()的工作:当程序调用getchar()时,库函数会查抄缓冲区是否有待读取的字符。假如有,getchar()会返回一个字符并从缓冲区中删除它。假如缓冲区为空,程序将等候用户输入,直到用户按下回车键。
按下回车键:回车键(通常是换行符 \n)是一个特殊字符,表现输入的竣事。当用户按下回车键时,操纵系统将换行符也放入缓冲区,而且getchar()会读取并返回此字符。
程序控制:getchar()函数阻塞程序执行,直到读取到输入的字符。因此,程序会暂停执行,等候用户输入。对于异步输入,程序需要处理I/O的等候(即阻塞),或者通过非阻塞I/O或信号处理等方式来处理。
8.5本章小结
本章围绕hello的IO管理展开。首先是理论层面——Linux的IO设备管理方法和Unix IO接口及其函数,然后是两个具体的函数的实现分析——printf和getchar。在完成了这一章后,我对IO管理有了一个清晰的熟悉。
结论
[*]hello最早诞生在源代码hello.c中。
[*]在预处理阶段,预处理器修改hello.c,得到hello.i。
[*]在编译阶段,hello.i被翻译成文本文件hello.s。
[*]在汇编阶段,汇编语言被转换为机器代码,生成可重定位目的文件hello.o。
[*]在链接阶段,hello.o和库文件组合成可执行文件hello。
[*]在操纵系统中执行这个二进制可执行文件时,操纵系统会利用fork()和execve()来创建一个新的进程。此时,程序从静态代码变成了一个进程。进程会获得系统资源并开始执行。
[*]在这个过程中,操纵系统的进程管分析分配时间片,调度该进程在CPU上执行,并确保它有充足的内存(通过虚拟内存管理)。同时,操纵系统会通过内存管理单元(MMU)为进程分配虚拟地址(VA)并映射到物理内存地址(PA)。假如程序访问的数据不在缓存中,操纵系统会通过页表和TLB等机制进行管理,确保高效的数据访问。
[*]操纵系统会通过各种I/O管理机制(如文件系统、硬件设备驱动程序等)来包管程序能够顺遂访问硬盘、表现器、键盘等外设。
[*]当程序完成输出并竣事时,操纵系统会整理进程的资源,开释内存,关闭文件形貌符等,进程会正常退出。此时,进程状态变为“停止”,系统将其从内存中移除。这时,Hello的“生命”彻底竣事。
大作业真的非常故意义,它帮助我对课上所学的知识进行了巩固,同时让我感受到了学习计算机的乐趣。固然在完成大作业的过程中碰到了许多的困难,但我都想办法一一降服了,让我的本领得到了提拔。在未来,我将利用幸亏计算机系统这门课程中学到的钻研精神,向着本身感兴趣的方向不断探索。
附件
名字
作用
hello.i
对hello.c预处理后的文件
hello.s
对hello.i编译后生成的文本文件
hello.o
对hello.s汇编后生成的可重定位目的文件
hello
链接后生成的可执行目的文件
1.elf
hello.o的ELF文件
2.asm
hello.o的反汇编文件
3.elf
hello的ELF文件
4.asm
hello的反汇编文件
参考文献
Randal E.Bryant,David O'Hallaron. 深入理解计算机系统. 机械工业出版社,2016.7.
深入解析:execve函数的工作原理与安全实践,-CSDN博客. 深入理解execve函数.
C/C++预处理过程详细梳理(预处理步调+宏界说#define/#include+inline函数+宏展开序次+条件预处理+别的预处理界说)_include和宏先后-CSDN博客. C/C++预处理过程详细梳理(预处理步调+宏界说#define/#include+inline函数+宏展开序次+条件预处理+别的预处理界说)
[转]printf 函数实现的深入分析 - Pianistx - 博客园.
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]