第1章 概述
1.1 Hello简介
P2P:即从步伐到进程(From Program to Process)。指的是将hello.c这个C语言源文件转换为运行时进程的过程。为了让hello.c运行起来,必须先将其转化为可执行文件,这一转化过程分为四个阶段:预处理、编译、汇编和链接。完成这些步调后,会生成一个可执行文件,随后可以在shell中运行它,运行时shell会为其分配进程空间。
020:即从零到零(From Zero-0 to Zero-0)。表示最初内存中没有与hello文件相干的内容。当shell通过execve函数启动hello步伐时,会将捏造内存映射到物理内存,并从步伐的入口加载和运行,进而执行main函数中的目标代码。当步伐执行结束时,shell的父进程会回收hello进程,而内核也会清除与hello文件相干的全部数据布局。
1.2 环境与工具
硬件环境:
处理器:12th Gen Intel(R) Core(TM)i5-12500H 2.50 GHz
机带RAM:16.0GB
系统范例:64位操作系统,基于x64的处理器
软件环境:Windows10 64位,VMware,Ubuntu 20.04 LTS
开发与调试工具:Visual Studio 2022 64位;vim objump edb gcc readelf等工具。
1.3 中心效果
hello.i 预处理后得到的文本文件
hello.s 编译后得到的汇编语言文件
hello.o 汇编后得到的可重定位目标文件
hello.asm 反汇编hello.o得到的反汇编文件
hello1.asm 反汇编hello可执行文件得到的反汇编文件
1.4 本章小结
本章先对hello的P2P和020流程进行了介绍,涵盖了流程的设计理念和详细实现方式;随后,对本实验所需的硬件环境、软件平台、开发工具进行了详细阐明,并列举了实验过程中生成的各个中心效果文件的名称及其作用。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理是指在步伐运行之前,由预处理器对源代码文件进行初步加工的过程。其重要使命是完成代码文本的替换,处理以#开头的指令,同时清除源代码中的注释及多余的空白字符。预处理指令通常以#开头,可以简单理解为这些指令会被替换为现实代码中对应的内容。
2.1.2 预处理的作用
预处理并不会直接分析源代码,而是通过对其进行分割、处理和替换来完成以下几项重要功能:
头文件包罗:将代码中引用的头文件指令替换为头文件中的内容。
宏界说:将宏界说的标识符替换为现实的代码内容。
条件编译:根据编译条件来决定某段代码是否参与编译。
其他处:如删除注释和多余的空白字符。
预处理是一个以文本插入和替换为核心的初步加工过程。
2.2在Ubuntu下预处理的命令
预处理的命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理效果分析
在Linux系统中打开hello.i文件后,我们将其与源步伐进行了对比。对比效果表明,源步伐中的预处理指令被扩展成了数千行内容,而步伐的其他部门则完全保持不变。这一现象验证了.c文件在预处理阶段确实被修改过。
在main函数代码出现之前的大量代码泉源于头文件<stdio.h>、<unistd.h>和<stdlib.h>的逐步睁开。
以stdio.h的睁开为例:在预处理阶段,#include指令的作用是将指定的头文件内容直接插入到源文件中。stdio.h是一个标准输入输出库头文件,界说了用于文件读写、标准输入输出的函数原型和宏界说等内容。
当预处理器遇到#include<stdio.h>时,它会在系统的头文件路径中搜索stdio.h文件,通常路径为/usr/include目录。找到文件后,预处理器会将stdio.h的内容复制到当前源文件中。而stdio.h内大概还包罗其他头文件的#include指令,比方#include<stddef.h>或#include<features.h>等,这些头文件也会被递归睁开并插入到源文件中。
需要注意的是,预处理器并不会对头文件的内容进行任何情势的计算或转换,它仅仅完成简单的复制与替换操作。
2.4 本章小结
本章介绍了在Linux环境下使用命令对C语言步伐进行预处理的详细方法,同时阐述了预处理的界说及其功能。通过一个简单的hello步伐作为示例,演示了从hello.c生成预处理文件hello.i的全过程,并对生成的代码进行了详细分析。分析效果表明,预处理后的文件hello.i包罗了标准输入输出库stdio.h的内容、宏和常量的界说,以及一些行号信息和条件编译指令等内容。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的界说
编译是指将用高级编程语言编写的源代码,翻译为等价的汇编语言格式步伐的过程。
3.1.2 编译的目的
编译的重要目的是将高级语言的源步伐转换为汇编语言代码,从而提高编程的效率和步伐的可移植性。编译的基本流程包括以下几个阶段:词法分析、语法分析、语义分析、中心代码生成、代码优化以及目标代码生成。
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s
3.3 Hello的编译效果分析
3.3.1汇编初始部门
在main函数前有一部门字段展示了节名称:
.file 声明出源文件
.text 表示代码节
.section .rodata 表示只读数据段
.align 声明对指令大概数据的存放地址进行对齐的方式
.string 声明一个字符串
.globl 声明全局变量
.type 声明一个符号的范例
3.3.2 数据部门
(1)字符串步伐有两个字符串存放在只读数据段中,如图:
在`hello.c`步伐中,唯一的数组是`main`函数的第二个参数 `char** argv`,该数组的每个元素都是指向字符范例的指针。根据分析,数组的起始地址存储在栈中的 `-32(%rbp)` 位置,而且在步伐运行中被两次调用作为参数转达给 `printf` 函数。
如图所示,在这两次调用中,`rdi` 寄存器分别被设置为两个字符串的起始地址,用于完成字符串的输出操作。这表明在步伐执行过程中,`printf` 函数通过`rdi`寄存器接收字符串地址,以输出对应的内容。
(2)参数argc
`argc`是`main`函数的第一个参数,它的值存储在寄存器`%edi`中。从相干语句可以看出,`%edi`寄存器中的值被压入了栈中。而从另一条语句可以得知,栈中该地址上的数值与立即数5进行了巨细比较,通过此过程可以确认`argc`的值最初保存在寄存器`%edi`中,随后被压入栈中以供后续操作使用。
(3)局部变量
在步伐中,唯一的局部变量是`i`。根据相干信息可知,局部变量`i`被存储在栈的`-4(%rbp)`位置。这表明变量`i`在运行时通过栈帧进行管理,位置位于基址指针`%rbp`的偏移量`-4`处。
3.3.3全局函数
hello.c中只声明确一个全局函数int main(int arge,.char*argv[]),我们通过汇编代码可知。
3.3.4赋值操作
在`hel1o.c`中,`for`循环起始的赋值操作`i = 0`在汇编代码中表现为`mov`指令。详细来说,由于`i`是一个`int`型变量,占用32位(4字节),因此在汇编中使用`movl`指令(转达双字)来完成赋值操作。这种指令将立即数`0`加载到变量`i`对应的内存或寄存器位置,从而实现赋值。
3.3.5算术操作
在hello.c中,for循环的算术操作是每次循环结束后的i++。在汇编代码中,这一操作通过add指令实现。由于变量i是一个int型的32位变量,因此汇编中使用addl指令(对双字操作)来完成自增操作。
3.3.6关系操作
hello.c中存在两个关系操作,分别为:
- 条件判断语句if(argc!=4):汇编代码将这条代码翻译为:
使用了cmp指令比较立即数5和参数argc巨细,而且设置了条件码。根据条件码,如果不相等则执行该指令背面的语句,否则跳转到.L2。
- 在for循环每次循环结束要判断一次i<10,判断循环条件被翻译为:
同(1),设置条件码,并通过条件码判断跳转到什么位置。
3.3.7控制转移指令
设置过条件码后,通过条件码来进行控制转移,在本步伐中存在两个控制转移:
(1)
判断argc是否为5,如果不为5,则执行if语句,否则执行其他语句,在汇编代码中则表现为如果条件码为1,则跳到.L2,否则执行cmpl指令后的指令。
(2)
在for循环每次结束判断一次i<10,翻译为汇编语言后,通过条件码判断每次循环是否跳转到.L4。而在for循环初始要对i设置为0,如下:
然后直接无条件跳转到.L3循环体。
3.3.8函数操作
(1) main函数
参数转达:
`main`函数的参数包括`int argc`和`char* argv[]`。关于参数的详细转达地址和值,已在前文详细阐述过。
函数调用:
通过`call`指令实现函数调用。调用时,将目标函数的地址写入栈中,并自动跳转到目标函数内部执行。在`main`函数中,调用了`printf`、`exit`和`sleep`函数。
局部变量:
局部变量`i`用于`for`循环。关于局部变量的地址和值的存储位置,已在前文分析过(栈中的`-4(%rbp)`位置)。
(2) printf函数
参数转达:
`printf`函数调用时的参数是`argv[1]`和`argv[2]`。
函数调用:
`printf`函数在`main`函数中被调用了两次:
1. 第一次:将寄存器`%rdi`设置为待转达字符串`"用法:Hello学号姓名 秒数!\n"`的起始地址。
2. 第二次:将寄存器`%rdi`设置为字符串`"Hello %s %s\n"`的起始地址。
参数转达的详细寄存器分配:
- 使用寄存器`%rsi`转达`argv[1]`。
- 使用寄存器`%rdx`转达`argv[2]`。
这些细节已在前文讲解过,参数通过寄存器转达并与`printf`函数内部处理逻辑相结合。
(3)exit函数
参数转达与函数调用:
将rdi设置为1,再使用call指令调用函数。
(4)atoi、sleep函数
参数转达与函数调用:
可见,atoi函数将参数argv[3]放入寄存器%rdi中用作参数转达,简单使用call指令调用。
然后,将转换完成的秒数从%eax转到达%edi中,edi存放sleep的参数,再使用call调用。
(5)getchar函数
无参数转达,直接使用call调用即可。
3.3.9范例转换
atoi函数将宁字符中转换为sleep函数需要的整型参数.
3.4 本章小结
本章重要介绍了C编译器如何将`hello.i`文件转换为`hello.s`文件的过程。内容包括:
1. 编译的界说和功能
编译是将预处理后的文件(如`hello.i`)转换为汇编代码文件(如`hello.s`)的过程。其功能是将高级语言的逻辑布局转化为更靠近底层硬件的汇编语言,从而为下一步的汇编和链接做好预备。
2. 编译的指令演示
通过详细的编译指令,展示了从预处理文件到汇编代码文件的转换过程。
3. 汇编代码分析
对生成的`hello.s`文件中的汇编代码进行了详细分析,探究了以下内容:
数据处理:包括变量的存储、加载和操作。
函数调用:分析了如何通过寄存器转达参数及调用子步伐。
赋值与运算:探究了赋值操作以及算术运算、关系运算的实现方式。
控制跳转:分析了条件分支和循环跳转的实现。
范例转换:阐明确不同范例之间如何进行转换操作。
4.源代码与汇编代码的对比
通过对比源代码和汇编代码,分析了两者在实现上述操作时的详细方式及其差异,资助理解汇编语言如何映射高级语言的语义。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是指通过汇编器(`as`)将包罗汇编语言的`.s`文件翻译为呆板语言指令的过程。这些呆板语言指令随后被打包成一个可重定位目标文件(`.o`文件)的格式。生成的`.o`文件是一个二进制文件,此中包罗`main`函数的指令编码。
4.1.2 汇编的作用
汇编的作用是将高级语言步伐转化为呆板可以直接辨认并执行的代码文件。汇编器将`.s`汇编步伐翻译成呆板语言指令,并将这些指令打包为可重定位目标步伐的格式。终极生成的`.o`文件是二进制格式,包罗步伐的指令编码。
4.2 在Ubuntu下汇编的命令
在Ubuntu系统下,对hello.s进行汇编的命令为:
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
4.3 可重定位目标elf格式
在shell中输入readelf -a hello.o > hello.elf 指令得到 hello.o 文件的 ELF 格式:
检察ELF格式文件的内容
(1)ELF头
ELF头(ELF header)以一个l6字节的序列开始,这个序列描述了生成该文件的系统的字的巨细和字节顺序。ELF头剩下的部门包罗了资助链接器语法分析和表明目标文件的信息,此中包括ELF头的巨细、目标文件的范例(如可重定位、可执行大概共享的)、呆板范例(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的巨细和数目。不同节的位置和巨细是有节头部表描述的,此中目标文件中每个节都有一个固定巨细的条目(entry)。ELF头展示如下:
(2)节头(section header)
记录各节名称、范例、地址、偏移量、巨细、全体巨细、旗标、链接、信息、对齐。
(3)重定位节
.rel.text节是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数大概引用全局变量的指令都需要修改,而调用当地函数的指令不需修改。可执行目标文件中不包罗重定位信息。如图,需要重定位的内容如下:
(4)符号表
.symtab节中包罗ELF符号表,这张符号表包罗一个条目的数组,存放一个步伐界说和引用的全局变量和函数的信息。该符号表不包罗局部变量的信息。符号表如下:
4.4 Hello.o的效果分析
4.4.1命令
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
与hel1o.s的对照分析:
(1)增加呆板语言
每一条指令增加了一个十六进制的表示,即该指令的呆板语言。比方,在hello.s中的一个cmpl指令表示为
而在反汇编文件中表示为
(2)操作数进制
反汇编文件中的全部操作数都改为十六进制。如(1)中的例子,立即数由hello.s中的$5变为了$0x5,地址表示也由-20(%rbp)变为-0x14(%rbp)。可见只是进制表示改变,数值未发生改变。
(3)分支转移
反汇编的跳转指令中,全部跳转的位置被表示为主函数+段内偏移量这样确定的地址,而不再是段名称(比方.L3)。比方下面的jmp指令,反汇编文件中为
而hello.s文件中为
(4)函数调用
反汇编文件中对函数的调用与重定位条目相对应。观察下面两个call指令调用函数,在hello.s中为
而在反汇编文件中调用函数为
在可重定位文件中call背面不再是函数名称,而是一条重定位条目指引的信息。
4.5 本章小结
本章内容概述
本章重要介绍了汇编的含义和功能,并结合现实操作阐明确汇编语言到呆板语言的转换过程及链接预备工作。以下是详细内容:
1. 汇编的含义与功能
汇编的核心在于将汇编语言的代码(如`hello.s`)转换为呆板语言指令,并生成可重定位目标文件(如`hello.o`)。终极目标是生成ELF格式的可执行文件(如`hello.elf`)。
2. 以Ubuntu系统下的`hello.s`文件为例
阐明确如何从汇编代码文件`hello.s`生成目标文件`hello.o`。
使用汇编器(如`as`)将`.s`文件汇编为`.o`文件。
使用链接器(如`ld`或`gcc`)将目标文件生成ELF格式的可执行文件`hello.elf`。
3. 分析ELF文件格式
将生成的可重定位目标文件改为ELF格式后,观察文件内容。
对文件中的每个节(Section)进行简单分析,阐明其作用和内容。
4. 反汇编分析
通过分析`hello.o`的反汇编代码(保存在`hello.asm`中)和原始汇编代码`hello.s`的区别与相同点。
比较两者,资助理解从汇编语言到呆板语言的转换过程。
阐明呆板语言如何做好链接的预备工作,包括符号表、重定位信息等内容。
重点总结
通过以上内容,用户可以清楚地理解汇编语言到呆板语言的转换过程,以及呆板为了链接可执行文件而做的预备工作。这一过程从汇编文件(`.s`)到目标文件(`.o`),再到可执行文件(`.elf`),涵盖了汇编、链接和生成可执行文件的完整流程。
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接(Linking)是将各种代码和数据片段收集并组合为一个单一文件的过程,这个文件可以被加载(复制)到内存中并执行。链接可以在以下阶段执行:
1. 编译时(Compile Time):即源代码被翻译为呆板代码时完成链接。
2. 加载时(Load Time):步伐被加载器加载到内存并执行时完成链接。
3. 运行时(Runtime):步伐运行期间动态完成链接(如动态库的加载)。
5.1.2 链接的作用
在当代系统中,链接由链接器(Linker)步伐自动执行,其作用包括:
1. 支持分离编译:链接使得分离编译成为大概,我们可以将大型应用步伐拆分为多个小的模块,每个模块可以独立开发、管理和编译。
2. 灵活性与可维护性:当某个模块需要修改时,仅需重新编译该模块,然后重新链接整个应用步伐,而不必重新编译其他模块。
3. 提高效率:通过分离编译和链接,可以减少编译时间并提高代码维护的效率。
5.2 在Ubuntu下链接的命令
在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分析hello的ELF格式,得到hello的节信息和段信息:
(1)ELF头(ELF Header)
hello1.elf中的ELF头与hello.elf中的ELF头包罗的信息种类基本相同,以描述了生成该文件的系统的字的巨细和字节顺序的16字节序列Magic开始,剩下的部门包罗资助链接器语法分析和表明目标文件的信息。与hello.elf相比较,hello1.elf中的基本信息未发生改变(如Magic,种别等),而范例发生改变,步伐头巨细和节头数目增加,而且得到了入口地址。
(2)节头
描述了各个节的巨细、偏移量和其他属性。链接器链接时,会将各个文件的相同段归并成一个大段,而且根据这个大段的巨细以及偏移量重新设置各个符号的地址。
(3)步伐头
步伐头部门是一个布局数组,描述了系统预备步伐执行所需的段或其他信息。
(4)Dynamic section
(5)Symbol table
符号表中保存着定位、重定位步伐中符号界说和引用的信息,全部重定位需要引用的符号都在此中声明。
5.4 hello的捏造地址空间
观察步伐头的LOAD可加载的步伐段的地址为0x400000。如图:
使用edb打开hello从Data Dump窗口观察hello加载到捏造地址的环境,查
看各段信息。如图:
步伐从地址0x400000开始到0x401000被载入,捏造地址从0x4000000x400f0结束,根据5.3中的节头部表,可以通过edb找到各段的信息。
如.interp节,在hello.elf文件中能看到开始的捏造地址:
在edb中找到对应的信息:
同样的,我们可以找到如.text节的信息:
5.5 链接的重定位过程分析
分析hello与hello.o区别
在Shell中使用命令objdump -d -r hello > hello1.asm生成反汇编文件hello1.asm
与第四章中生成的hello.asm文件进行比较,其不同之处如下:
(1)链接后函数数目增加
链接后的反汇编文件hello1.asm中,多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。
(2)函数调用指令call的参数发生变化
在链接过程中,链接器分析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完整的反汇编代码。
(3)跳转指令参数发生变化
在链接过程中,链接器分析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。
重定位过程
重定位由两步构成:
(1)重定位节和符号界说。在这一步中,链接器将全部相同范例的节归并为同一范例的聚合节。然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模块界说的每个节,以及赋给输入模块界说的每个符号。至此步伐中每条指令和全局变量都有唯一的运行内存地址。
(2)重定位节中的符号引用。这一步中链接器修改代码节和数据节中对每个符号的引用,使得它们指向精确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据布局。
(3)重定位过程地址计算方法如下:
5.6 hello的执行流程
通过edb的调试,一步一步地记录下call命令进入的函数。
(I)开始执行:_start、_libe_start_main
(2)执行main:_main、printf、_exit、_sleep、getchar
(3)退出:exit
子步伐名或地址
步伐名 步伐地址
_start 0x4010f0
_libc_start_main 0x2f12271d
main 0x401125
_printf 0x4010a0
_sleep 0x4010e0
_getchar 0x4010b0
_exit 0x4010d0
5.7 Hello的动态链接分析
动态链接的基本思想是把步伐按照模块拆分成各个相对独立部门,在步伐运行时才将它们链接在一起形成一个完整的步伐,在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为界说它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在步伐加载的时间再分析它。延迟绑定是通过GOT和PLT实现的,根据hello.elf文件可知,GOT起始表位置为:0x404000:
GOT表位置在调用dl_init之前0x404008后的16个字节均为0:
调用了dl_init之后字节改变了:
在步伐运行过程中,变量和库函数的地址计算和访问依赖于以下机制:
1. 变量的地址计算
对于变量,利用代码段和数据段相对位置不变的特点,可以通过基于偏移量的计算方式获取精确的内存地址。无论步伐被加载到内存的哪个位置,只要保持代码段和数据段的相对布局同等,就能精确访问变量。
2. 库函数的动态链接:PLT 和 GOT
动态链接过程中,涉及到过程链接表(PLT)和全局偏移量表(GOT)的相助。以下是工作机制:
1. 初始状态
PLT(Procedure Linkage Table):存储一组跳转代码,用于间接调用库函数。
GOT(Global Offset Table):初始时存储指向PLT内部第二条指令的位置。
2. 第一次调用库函数时
调用PLT的跳转代码,PLT会跳转到GOT所指示的地址。 此时,GOT指向的是PLT中的特定指令,链接器被调用以分析函数地址。链接器会将现实的库函数地址写入GOT表中。
3. 后续调用库函数时
当再次调用PLT时,GOT中的条目已经被修改为库函数的真实地址。
PLT直接跳转到GOT中存储的真实地址,从而完成调用,无需再次分析。
5.8 本章小结
链接的概念与作用
1. 链接的概念
链接是将各个代码和数据片段组合为一个可以加载和运行的文件的过程。它可以在编译时、加载时或运行时完成。
2. 链接的作用
分离编译:支持将大型步伐拆分为多个模块独立编译。
灵活维护:修改某模块后,只需重新编译该模块并重新链接整个步伐。
动态链接:通过运行时加载共享库,提高内存和存储效率。
链接过程分析
使用命令链接生成可执行文件`hello`,并观察其ELF格式内容。
利用工具(如`edb`)分析步伐的捏造地址空间布局。
通过`hello`步伐,分析重定位、执行过程,以及动态链接中PLT和GOT的协作机制。
总结:本章通过实例演示,清楚展示了链接的实现过程及其在步伐运行中的重要作用。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程是一个执行中步伐的实例,是计算机系统中步伐在某数据聚集上的一次运行运动。
资源分配与调度单位:进程是操作系统中进行资源分配和调度的基本单位。
操作系统的基础布局:在传统操作系统中,进程既是资源分配的基本单元,也是步伐执行的基本单元。
6.1.2 进程的作用
进程为步伐提供了一种独占处理器和内存的假象:
处理器似乎一连无间断地执行步伐中的指令。
每个步伐都运行在某个进程的上下文中,确保步伐的执行独立性和安全性。
总结来说,进程是操作系统提供给步伐计算资源的抽象,是步伐执行的基本环境。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell-bash的作用
Shell是一个交互型应用级步伐,也被称为命令分析器,它为用户提供一个操作界面,担当用户输入的命令,并调度相应的应用步伐。
6.2.2 Shell-bash的处理流程
首先从终端读入输入的命令,对输入的命令进行分析,如果该命令为内置命令,则立即执行命令,否则调用fork创建一个新的子进程,在该子进程的上下文中执行指定的步伐。判断该步伐为前台步伐还是后台步伐,如果为前台步伐则等候步伐执行结束,若为后台步伐则将其放回后台并返回。在过程中shell可以担当从键盘输入的信号并对其进行处理。
6.3 Hello的fork进程创建过程
指令执行流程概述
1. 用户输入指令
用户在Shell界面输入指令:`./hello 2021113211 郑文翔`。
2. Shell判断指令范例
Shell判断该指令不是内置命令,因此需要执行外部步伐。
3. 父进程调用`fork`函数
父进程通过`fork`函数创建一个新的子进程:
- 子进程得到与父进程用户级捏造地址空间相同的副本,包括以下内容:
- 代码段:步伐的指令聚集。
- 数据段:全局变量、静态变量等。
- 堆:动态分配的内存区域。
- 共享库:动态链接的库文件。
- 用户栈:用于函数调用和局部变量存储。
4. 父子进程的区别
- PID(进程ID):父进程和子进程的PID不同。
- `fork`返回值:
- 在父进程中,`fork`返回子进程的PID,用于区分子进程。
- 在子进程中,`fork`返回值为0,用于区分自己是子进程。
- 返回值的差异提供了一种明确的方法,来分辨步伐是在父进程中运行还是在子进程中运行。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个步伐。函数声明如下:
int execve(const char *filename, const char *argv[], const char *envp[]);
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,比方找不到filename,execve才会返回到调用步伐。以是,与fork一次调用返回两次不同,execve调用一次并不返回。main函数运行时,用户栈的布局如图所示:
6.5 Hello的进程执行
当 `hello` 步伐运行时,进程为应用步伐提供了以下抽象:
1. 独立的逻辑控制流:进程表现得像是独占使用处理器,提供了独立的指令执行序列。
2. 私有的地址空间:进程看似独占地使用内存,避免受到其他步伐的干扰。
操作系统则提供了一些关键的运行抽象:
1. 逻辑控制流:通过调试器单步执行步伐时,可以观察到一系列步伐计数器(PC)的值,这些值对应于步伐的可执行文件或动态链接的共享对象中的指令序列。这样的指令序列被称为逻辑控制流,或逻辑流。若两个逻辑流在时间上重叠运行,则称为并发流。
2. 上下文切换:操作系统通过一种称为上下文切换的机制,实现进程间的多使命调度。内核为每个进程维护其上下文信息,用以重新启动被抢占的进程。
3. 时间片:每个进程在 CPU 上运行一段时间,称为时间片。多使命运行机制也被称为时间分片。
4. 用户模式与内核模式:处理器通过某个控制寄存器的模式位区分两种运行模式。在内核模式下,进程可以执行全部指令并访问任意内存位置;在用户模式下,进程只能执行非特权指令,且无法直接访问内核区的内存数据。
5. 上下文信息:上下文是内核重新启动被抢占进程所需的状态信息,内容包括通用寄存器、浮点寄存器、步伐计数器、用户栈、状态寄存器、内核栈,以及各种内核数据布局。
在 `hello` 步伐运行期间,`execve` 系统调用会为其分配新的捏造地址空间。步伐最初运行在用户模式,调用 `printf` 输出 `"Hello 2021113211 郑文翔"`。随后调用 `sleep` 进入内核模式,运行信号处理步伐,并返回用户模式继承执行。运行过程中,CPU 会不停切换上下文,将步伐的执行过程划分为多个时间片,与其他进程交替使用 CPU,从而实现多使命调度。
6.6 hello的异常与信号处理
6.6.1异常的分类
6.6.2运行效果及相干命令
(1)正常运行状态
输入命令:./hello 2023112910 孙云飞 18980733134 1
(2)运行时按下Ctrl + C
按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。
(3)运行时按下Ctrl + Z
按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。
(4)对hello进程的挂起可由ps和jobs命令检察,可以发现hello进程确实被挂起而非被回收,且其job代号为1。
(5)在Shell中输入pstree命令,可以将全部进程以树状图显示:
(6)输入kill命令,则可以杀死指定(进程组的)进程:
(7) 输入fg 1则命令将hello进程再次调到前台执行,可以发现Shell首先打印hello的命令行命令,hello再从挂起处继承运行,打印剩下的语句。步伐仍然可以正常结束,并完成进程回收。
(8)不停乱按
在步伐执行过程中乱按所造成的输入均缓存到stdin,当getchar的时间读出一个’\n’结尾的字串(作为一次输入),hello结束后,stdin中的其他字串会当做Shell的命令行输入。
6.7本章小结
本章通过一个简单的 `hello` 步伐,深入探究了计算机系统中进程和 Shell 的核心概念。首先介绍了进程的界说、作用以及 Shell 的功能和处理流程,然后详细分析了 `hello` 步伐从进程创建、加载到执行的全过程,并对大概出现的异常环境和运行效果进行了阐明,资助读者更好地理解操作系统中进程调度和步伐运行的基本原理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1. 逻辑地址
- 逻辑地址是指步伐访问时指令给出的地址,也称为相对地址。
- 它需要通过寻址方式的计算或变更,才气得到物理地址。
- 逻辑地址由段标识符和段内偏移量构成,比方步伐 `hello` 产生的段相干偏移地址。
2. 线性地址
- 线性地址是逻辑地址转换为物理地址的中心效果。
- 在分段机制中,逻辑地址的偏移量与段基址相加,得到线性地址。
- 步伐 `hello` 的代码逻辑地址会通过此过程生成线性地址。
3. 捏造地址
- 步伐访问存储器时使用的逻辑地址又称为捏造地址。
- 捏造地址经过地址翻译机制才气映射到物理地址,与现实物理内存容量无关。
- 对于 `hello` 步伐,捏造地址是步伐运行时使用的地址。
4. 物理地址
- 物理地址是存储器中每个字节单元的唯一地址,也叫绝对地址。
- 它直接对应存储器的硬件地址,比方 `hello` 步伐的现实存储位置。
7.2 Intel逻辑地址到线性地址的变更-段式管理
段式管理是指把一个步伐分成多少个段进行存储,每个段都是一个逻辑实体。段式管理是通过段表进行的,包括段号(段名)、段出发点、装入位、段的长度等。步伐通过分段划分为多个块,如代码段、数据段、共享段等。
一个逻辑地址是两部门构成的,包括段标识符和段内偏移量。段标识符是由一个16位长的字段构成的,称为段选择符。此中前13位是一个索引号,后3位为一些硬件细节。索引号便是“段描述符”的索引,段描述符详细地址描述了一个段,许多个段描述符就构成了段描述符表。通过段标识符的前13位直接在段描述符表中找到一个详细的段描述符。
全局描述符表(GDT)整个系统只有一个,它包罗:(1)操作系统使用的代码段、数据段、堆栈段的描述符(2)各使命、步伐的LDT(局部描述符表)段。
每个使命步伐有一个独立的LDT,包罗:
(1)对应使命/步伐私有的代码段、数据段、堆栈段的描述符
(2)对应使命/步伐使用的门描述符:使命门、调用门等。
7.3 Hello的线性地址到物理地址的变更-页式管理
捏造内存被设计为一个由磁盘上的 N 个一连字节构成的逻辑数组。捏造内存系统通过分页机制将捏造内存划分为多少捏造页,同时也将物理内存划分为相应的物理页。
为了管理捏造页,系统利用**页表**,它是一个由页表条目(PTE)构成的数组。
- 每个 PTE 包罗两个关键字段:有效位和地址字段。
- 有效位:指示该捏造页是否被缓存在 DRAM 中。
- 如果有效位为 1,表示捏造页已加载到物理内存中,地址字段存储相应物理页的起始位置。
- 如果有效位为 0(即缺页),系统会从磁盘中读取相应的捏造页并加载到物理内存中。
通过页表的管理,捏造内存系统有效地实现了捏造页到物理页的映射,支持按需加载和内存保护等功能。MMU利用页表来实现从捏造地址到物理地址的翻译。
下面为页式管理的图示:
7.4 TLB与四级页表支持下的VA到PA的变更
Core i7接纳四级页表的层次布局。CPU产生捏造地址VA,捏造地址VA传送给MU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。如果掷中,则得到物理地址PA。如果TLB中没有掷中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,终极在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。工作原理如下:
多级页表的工作原理展示如下:
7.5 三级Cache支持下的物理内存访问
如图为高速缓存存储器组织布局:
高速缓存的布局将m个地址位划分成了t个标记位,s个组索引位和b个块偏移位:
如果选中的组存在一行有效位为1,且标记位与地址中的标记位相匹配,我们就得到了一个缓存掷中,否则就称为缓存不掷中。如果缓存不掷中,那么它需要从存储器层次布局的下一层中取出被哀求的块,然后将新的块存储在组索引位指示组中的一个高速缓存行中,详细替换哪一行取决于替换计谋,比方LRU计谋会替换末了一次访问时间最久远的那一行。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据布局,并分配给它一个唯一的PID。为了给这个新进程创建捏造内存,它创建了当前进程的mm_ struct、.区域布局和页表的原样副本。当fork在新进程中返回时,新进程现在的捏造内存刚好和调用fork时存在的捏造内存相同。当这两个进程中的任何一个。后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
`execve` 函数调用内核中的启动加载器代码,在当前进程中加载并运行可执行文件(如 `hello` 步伐),从而替换掉当前正在运行的步伐。加载并运行 `hello` 步伐的详细步调包括:
1. 删除已有用户区域
- 清空当前进程捏造地址空间用户区中的全部现有区域布局。
2. 映射私有区域
- 为新步伐的代码、数据、`.bss` 和栈区域创建新的区域布局,这些区域是私有的,并接纳写时复制机制。
- 将代码和数据区域映射到 `hello` 文件的 `.text` 和 `.data` 部门。
- `.bss` 区初始化为二进制零(哀求匿名映射),其巨细在 `hello` 文件中界说。
- 栈和堆区域同样初始化为二进制零,初始长度为零。
3. 映射共享区域
- 动态链接共享对象(如 `libc.so`)到步伐,并将其映射到用户捏造地址空间的共享区域中。
4. 设置步伐计数器
- 末了,`execve` 设置当前进程上下文的步伐计数器(PC),使其指向新加载代码区域的入口点,从而开始执行步伐。
通过这些步调,`execve` 有效地将 `hello` 步伐加载到当前进程中并启动其运行。
7.8 缺页故障与缺页中断处理
当步伐执行过程中发生缺页故障时,内核会调用缺页处理步伐来解决。处理步伐的执行步调如下:
1. 检查捏造地址是否合法
- 验证触发缺页的捏造地址是否在进程的地址空间范围内。
- 如果地址不合法,内核会触发段错误(`segmentation fault`),制止该进程。
2. 检查权限
- 确认进程是否拥有对该页面的读、写或执行权限。
- 如果进程无权访问,内核将触发保护异常(`protection fault`),同样制止步伐。
3. 页面调度与换入
- 如果地址和权限检查均通过,内核会选择一个物理页面作为断送页面。
- 如果该页面被修改过,内核会将其写回磁盘(换出)。
- 随后,从磁盘中读取新的页面(换入),并更新页表以反映新的映射关系。
4. 恢复执行
- 页表更新完成后,内核将控制权返回给步伐,重新执行导致缺页故障的指令。
通过这些步调,内核能够有效处理缺页故障,并继承执行步伐而不受干扰。
7.9动态存储分配管理
动态内存管理是指在步伐运行时根据需要分配和释放内存的机制,重要由操作系统和运行时库共同实现。以下是动态内存管理的基本方法与计谋:
基本方法
1. 分配内存
使用诸如 `malloc`、`calloc` 或 `realloc` 等函数来动态分配内存。
这些函数向堆(heap)中哀求一段一连的内存区域,并返回指向该区域的指针。
2. 释放内存
使用 `free` 函数释放不再需要的内存。
释放内存后,这部门内存可以被重新分配给其他步伐或对象。
3. 对齐与元数据管理
动态内存分配器会根据内存对齐要求调整分配的内存块巨细。
分配器通常在每个内存块的前面存储元数据(如块巨细、分配状态等),以便有效管理内存。
基本计谋
1. 首次适配(First Fit)
从空闲内存列表中找到第一个足够大的块进行分配。
优点:简单高效,缺点:容易产生内存碎片。
2. 最佳适配(Best Fit)
从空闲内存列表中找到最小的、刚好满意需求的块进行分配。
优点:减少浪费,缺点:搜索时间较长,大概导致更多碎片。
3. 最差适配(Worst Fit)
从空闲内存列表中找到最大的块进行分配,确保剩余空间更大。
优点:保存较大的空闲块,缺点:大概导致内存利用率下降。
4. 分区分配
将内存划分为巨细不同的分区,每个分区用于特定巨细的内存哀求。
减少碎片并提高分配效率。
5. 垃圾回收(Garbage Collection)
自动释放不再使用的内存块,常见于高级语言(如 Java 和 Python)。
优点:减少步伐员错误,缺点:增加系统开销。
常见问题及优化
1. 内存碎片
内存分配和释放大概导致大量碎片,降低内存利用效率。
优化步伐:归并相邻的空闲块或使用紧凑化技能。
2. 内存泄漏
步伐未释放已分配的内存,导致内存长期占用。
解决方法:养成精良的内存管理习惯或使用内存检测工具。
3. 性能优化
使用快速分配器(如分层分配器、内存池)减少分配和释放的开销。
动态内存管理是当代计算机步伐设计中不可或缺的一部门,合理应用这些方法与计谋可以显著提高步伐性能和内存利用效率。
7.10本章小结
本章重点讲解了 `hello` 步伐的存储器地址空间布局,涵盖了 Intel 的段式管理和页式管理的基本概念。在指定的 Intel Core i7 环境下,详细阐明确捏造地址(VA)到物理地址(PA)的转换过程以及物理内存的访问机制。别的,还分析了 `hello` 进程在 `fork` 系统调用时的内存映射方式、`execve` 调用时的内存重构,以及发生缺页故障时的处理流程和中断处理机制。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模子化:文件
设备管理:unix io接口
在Linux系统中,设备被抽象为文件,而全部的输入输出操作都被视为对这些文件的读写操作。一个Linux文件可以看作是一个由 m 个字节构成的序列:B0, B1, ..., Bk, ..., Bm-1。
通过这种将设备映射为文件的设计,Linux内核提供了一个简单且同一的应用接口,称为 Unix I/O 接口。借助这一接口,全部的输入输出操作都可以以同等的方式进行,无论是对普通文件、设备文件,还是其他范例文件的操作。这种优雅的模子化方法极大地简化了设备管理和开发工作。
8.2 简述Unix IO接口及其函数
在 Unix I/O 接口中,文件操作是通过一组系统调用完成的,以下是这些操作的基本概述:
1. 打开文件
- 应用步伐通过调用 `open` 函数向内核哀求打开文件,内核返回一个文件描述符(非负整数),用于标识该文件。
- 每个进程在启动时默认打开三个文件:标准输入(描述符 0)、标准输出(描述符 1)和标准错误(描述符 2),可使用 `<unistd.h>` 中的常量取代显式描述符值。
- 示例函数:
int open(char *filename, int flags, mode_t mode);
返回值为文件描述符,如果失败则返回 -1。
2. 改变文件位置
- 内核为每个打开的文件维护一个文件位置(字节偏移量),默认从 0 开始。
- 通过调用 `lseek` 函数,可以显式调整当前文件位置:
off_t lseek(int fd, off_t offset, int whence);
`whence` 可以是 `SEEK_SET`(从文件开头)、`SEEK_CUR`(从当前位置)或 `SEEK_END`(从文件结尾)。
3. 读写文件
- 使用 `read` 函数从文件中的当前文件位置读取数据到内存:
ssize_t read(int fd, void *buf, size_t n);
返回值是乐成读取的字节数,若到达文件结尾则返回 0,出错返回 -1。
- 使用 `write` 函数将数据从内存写入文件:
ssize_t write(int fd, const void *buf, size_t n);
返回乐成写入的字节数,出错返回 -1。
4. 关闭文件
- 当文件操作完成时,调用 `close` 函数通知内核释放相干资源:
int close(int fd);
乐成返回 0,出错返回 -1。
通过这些系统调用,Unix 提供了一个简单同一的接口,使得对文件和设备的操作可以以同等的方式进行。
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
字符串显示的完整过程分析
1. `printf` 函数调用
- `printf` 是步伐中格式化输出的入口函数。
- 它通过可变参数列表(`...`)接收格式化字符串和参数,并将这些参数传入 `vsprintf` 进行格式化处理。
- 详细过程:
- 通过 `va_list` 获取第一个可变参数的地址。
- 调用 `vsprintf` 生成格式化后的字符串,存储在缓冲区 `buf` 中。
- 使用 `write` 系统调用将缓冲区内容输出到屏幕。
2. `vsprintf` 格式化字符串
- `vsprintf` 的功能是根据格式化字符串(`fmt`)和参数列表(`args`)生成终极的输出字符串。
- 核心逻辑:
- 遍历格式化字符串,如果检测到 `%`,则根据后续的格式符(如 `%x`、`%s` 等)处理对应参数。
- 将参数转换为字符串(如整数转十六进制 `itoa`)并复制到输出缓冲区。
- 返回生成字符串的长度。
3. `write` 系统调用**
- `write` 是 Linux 提供的系统调用,用于将数据写入文件或设备。对于屏幕输出,`write` 将数据写入标准输出文件描述符(`STDOUT_FILENO`)。
- `write` 的反汇编指令:
```assembly
mov eax, _NR_write ; 系统调用号写入 eax
mov ebx, [esp + 4] ; 文件描述符写入 ebx
mov ecx, [esp + 8] ; 缓冲区地址写入 ecx
int INT_VECTOR_SYS_CALL ; 触发系统调用中断
```
- 系统调用通过 `int 0x80` 中断进入内核,执行对应服务。
4. `sys_call` 的处理
- 内核中断处理步伐 `sys_call` 接收 `write` 哀求。
- 核心动作:
- 通过总线将字符串的字节从寄存器复制到显卡的显存中。
- 显存中存储的是字符的 ASCII 码。
5. 字符显示驱动子步伐
- 显卡驱动步伐将显存中的 ASCII 码作为索引,查找字模库中对应的点阵信息。
- 点阵信息表示字符的像素分布,将其写入 VRAM(显存的一部门)。
- VRAM 中存储每个像素点的 RGB 颜色信息。
6. 显示芯片刷新
- 显示芯片按照预设的刷新频率逐行读取 VRAM 的内容。
- 每个像素点的 RGB 信息通过信号线传输到液晶显示屏。
- 液晶显示屏据此点亮对应的像素,终极显示出整个字符串。
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入分析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子步伐:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子步伐。担当按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到担当到回车键才返回。
8.5本章小结
本章通过介绍Linux的IO设备管理方法、Unix IO的接口及函数、printf和getchar的实现,让我们对hello执行过程中的IO管理有了一定的认识。
结论
计算机系统是人类智慧的结晶,其复杂性和精致性表现在每一个环节的设计中。比方,链接过程中引入了库的概念,特别是动态链接共享库的实现,使得库函数不再需要重复存储在每个步伐中,极大地节约了内存资源。高速缓存则充分利用步伐的局部性原理,结合了 CPU 的高速与内存的大容量,使数据访问更加高效。捏造内存作为内存管理和保护的核心工具,不仅简化了进程管理、链接与内存共享等问题,还提升了系统的稳定性和安全性。这些优化设计的相互作用,造就了当代计算机系统的高效与安全。
附件
文件名
| 功能
| hello.c
| 源步伐
| hello.i
| 预处理后得到的文本文件
| hello.s
| 编译后得到的汇编语言文件
| hello.o
| 汇编后得到的可重定位目标文件
| hello.elf
| 用readelf读取hello.o得到的ELF格式信息
| hello.asm
| 反汇编hello.o得到的反汇编文件
| hello1.asm
| 反汇编hello可执行文件得到的反汇编文件
| hello
| 可执行文件
|
参考文献
[1] Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.
[2] https://www.cnblogs.com/buddy916/p/10291845.html
[3] [转]printf 函数实现的深入分析 - Pianistx - 博客园
[4] printf背后的故事 - Florian - 博客园.
[5] linux2.6 内存管理——逻辑地址转换为线性地址(逻辑地址、线性地址、物理地址、捏造地址) - 刁海威 - 博客园
[6] https://blog.csdn.net/spfLinux/article/details/54427494
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |