程序人生-Hello’s P2P From Program to Process
计算机系统
大作业
题 目 程序人生-Hello’s P2P
学 号 2022112767
班 级 22wl02
学 生 符爱
指 导 教 师 刘宏伟
计算机科学与技术学院
2024年5月
本文主要介绍在linux发行版操作系统Ubantu下,C语言程序hello.c从源代码转换为可执行文件的过程,包括预处理、编译、汇编、链接、进程管理、存储管理和IO管理等整个生命周期。旨在通过对hello程序在计算机中运行过程中的分析,总结并深入理解程序在计算机中运行的基本原理。
目 录
第1章 概述
1.1 Hello简介
Hello是C语言中最基础的一个程序,可以通过运行hello的过程来了解程序在计算机中的处理、执行过程,大致可以总结为P2P和020两个过程:
P2P:From Program to Process 编译完成的C语言源程序(文本)hello.c经过预处理器(cpp)预处理之后得到hello.i文件,经过编译器(ccl)编译后得到汇编程序hello.s,经过汇编器(as)汇编后得到可重定位目标文件hello.o,最后经由链接器(ld)生成可执行程序hello,在壳(shell)里输入执行命令,进程管理(OS)为hello fork()一个进程(Process),即完成了P2P过程。
020:O2O: From Zero-0 to Zero-0进程管理为hello进行execve操作,然后经过mmap操作将其映射到内存中,给运行的hello分配时间片,让其在Hardware(CPU/RAM/IO)取指译码执行/流水线等,即执行逻辑控制流过程。程序运行结束后,父进程回收子进程,内核会删除程序相关数据,即完成了020过程。
1.2 环境与工具
硬件环境:12th Gen Intel(R) Core(TM) i5-12450H 2.00 GHz 16G RAM
软件环境:windows11 64位,VMware,Ubuntu 18.04
开发和调试工具:Visual Studio 2022 64位 CodeBlocks;vim objump edb gcc readelf等
1.3 中间结果
1.4 本章小结
本章首先介绍了hello程序的P2P和020过程,通过两过程的详细介绍,阐述了程序在计算机中运行的全过程,然后列出了撰写论文的软、硬件环境和开发、调试工具,最后列写出实现hello运行过程中产生的中间文件结果。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理指的是程序在编译之前进行的处理,是计算机在处理一个程序时所进行的第一步处理,可以进行代码文本的替换工作,但是不做语法检查。预处理是为编译做的准备工作,能够对源程序.c文件中出现的以字符“#”开头的命令进行处理,包括宏定义#define、文件包含#include、条件编译#ifdef等,最后将修改之后的文本进行保存,生成.i文件,预处理结束。
预处理的作用:
1.处理用#define定义的内容,替换为实际的字符;
2.处理#include预编译指令,将被包含的文件插入到该预编译指令的位置;
4. 处理#if等条件编译指令。
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E hello.c -o hello.i
图 2-1预处理命令
2.3 Hello的预处理结果解析
打开hello.i文件可以看到
图 2-2预处理结果
预处理后,引入了头文件并去除了注释,宏都被替换。添加了行号,编译指令被保留
2.4 本章小结
介绍了hello.c预处理生成hello.i的过程,该过程主要包括:头文件展开、宏定义替换、条件编译和注释删除。预处理器只进行简单的复制和替换操作,不对头文件中的内容进行计算和处理。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:编译器(ccl)将文本文件 hello.i 翻译成文本文件 hello.s ,它包含一个汇编语言程序
注:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s
图 3-1 编译指令
3.3 Hello的编译结果解析
3.3.1汇编指令
图 3-2汇编指令
.file “hello.c”//源文件
.text
.section .rodata
3.3.2 数据的处理
(1)字符串
图 3-3 字符串
图 3-4 printf() 的表达
main()函数中第一个与第二个printf()语句的内容在hello.s中的表达。
其一为printf("用法: Hello 2022112767 符爱 1998*****14 4!\n");
另一个为printf("Hello %s %s %s\n",argv[1],argv[2],argv[3])。
(2) int i
(3) int argc :
(4)数组char *argv[]
argv为数组首地址,main()访问数组元素。在hello.s中,%rax表示取地址。argv[1]和argv[2]作为for循环中printf的参数。由argv[1]和argv[2]的取值语句可知argv的首地址是-32(%rbp)。取argv[2]的汇编语句:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
(5)其他数据以立即数的形式出现。
3.3.2赋值
(1)循环语句中的i = 0,汇编如图
图 3-5 i=0的赋值语句
(2)MOV类的数据传送指令
指令
效果
描述
MOVS,D
D< -S
传送
movb
传送字节
movw
传送字
movl
传送双字
movq
传送四字
movabsqI,R
R< -I
传送绝对四字
表3-1 MOV类的数据传送指令
3.3.4. 控制转移的处理
(1)for循环
图3-6 for()循环的编译
.L2为开始前的赋值语句。.L4为for循环的判断语句,.L3之后.L4之前为进入循环体中的循环部分,循环10次。
(2)if(argc!=5) if语句
图3-7 if语句的编译
cmpl比较栈上的argv值和5,je为判断相等条件。如果为0,说明argv==5,跳转去执行for循环,否则执行if中的输出语句,输出提示。
(3)跳转指令
图 3-8 跳转指令
3.3.5. 函数操作
(1)main函数
参数传递:外部传入的参数为数组元素,分别存储在寄存器%rdi,%rsi中。
分配和释放内存则使用函数分配栈空间,结束时恢复栈空间
(2)printf函数
参数传递:第一次将学号姓名字符串的首地址存入%rdi,第二次用%rdi传递“Hello %s %s\n”的首地址,%rsi和%rdx为分别存储。
使用call语句跳转至printf函数:call printf@PLT , call puts@PLT
(3)getchar函数
call getchar@PLT
(4)exit函数
call exit@PLT
3.3.6. 数值操作及比较
(1)i++;
addl $1, -4(%rbp)
(2)i<10
.L3中:cmpl $9, -4(%rbp)
程序中作为循环判断条件,与i<10等价。
3.4 本章小结
总结了编译的概念、作用和部分编译命令。结合hello.i的编译结果,就C语言中的数据与操作如何被翻译成汇编语言进行了分析。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:
汇编指的是汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,包含hello程序执行的机器指令。
汇编的作用:
实现将汇编代码转换为机器指令,使之在链接后能够被计算机直接执行。
注:这儿的汇编是指从.s 到.o,即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
汇编指令:gcc -O hello.s -o hello.o
图 4-1汇编指令
4.3 可重定位目标elf格式
命令:readelf -a hello.o > hello.elf
(1)ELF头:
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,节头部表中条目的大小和数量等信息。
图 4-2ELF头
(2)节头
节头记录每个节的名称、类型、属性、在ELF文件中所占的长度、对齐方式和偏移量等。
图 4-3节头
(3)重定位节
重定位条目告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。如图,偏移量是需要被修改的引用的节偏移,符号标识被修改引用应该指向的符号。类型告知链接器如何修改新的引用,加数是一个有符号常数,一些类型的重定位要用它对被修改引用的值做偏移调整。
图 4-4重定位节
(4)符号表
图 4-5符号表
.dynsym节中包含ELF符号表,这张符号表包含一个条目的数组,存放一个程序定义和引用的全局变量和函数的信息。
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > hello.sam
图 4-6 hello.o的反汇编代码
分析hello.o的反汇编代码,并请与第3章的 hello.s进行对照分析,发现反汇编代码与hello.s差别不大,只是有几处不同:
机器指令hello.o的反汇编代码在左侧增添了机器指令的十六进制代码和地址
分支转移在hello.s中,代码由不同的段组成,每次跳转会跳到其他段的位置,如L1,L2,L3;hello.o的反汇编文件为跳转指令分配了地址。下一条指令的地址为地址寄存器的中的值加上指令操作数的结果。
函数调用在hello.o的反汇编代码中有很多函数调用,这些函数源自库或者其他的文件,必须在经过链接后才能调用。hello.o的反汇编文件为这些函数留下了地址以便链接后重定向。
全局变量:hello.s中使用段名称+%rIP访问,反汇编代码中使用0+%rIP访问。机器语言中待访问的全局变量地址为全0。在重定位节中有对应的重定位条目,链接之后确定地址。
4.5 本章小结
本章讲述了汇编语言的基本含义和功能,以hello.s的汇编文件为例,说明如何将其汇编成可重定位目标文件hello.o,然后将其链接成ELF格式的可执行文件 hello.elf。通过分析 hello.o 文件的反汇编代码以及原始汇编文件 hello.s 的区别和相同点,可以清晰地理解汇编语言到机器语言的转换过程。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以加载(复制)到内存并且执行。
链接的作用:
链接使分离编译成为可能,可以将大型的应用程序分为更小,更容易管理的模块,并独立的修改和编译这些模块。可以将公共函数聚合为单个文件
注:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
命令:ld hello.o -lc -o hello
图 5-1链接命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
命令:readelf -a hello > hello01.elf
5.3.1ELF头
hello的ELF头和hello.o的ELF头包含的信息种类基本相同,均以一个16字节序列开始,该序列描述了生成该文件的系统的字的大小和字节顺序的,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。与hello.elf相比较,hello01.elf中的基本信息未发生改变,但类型发生改变,程序头大小和节头数量增加,并且获得了入口地址。
图 5-2 ELF头
5.3.2节头
描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。
图 5-3节头
5.3.3程序头
图 5-4程序头
程序包含六个段。
1.PTDR: 指定程序头表在文件及程序内存映像中的位置和大小。
2.INTERP: 指定要作为解释程序调用的以空字符结尾的路径名的位置和大小。对于动态可执行文件,必须设置此类型。
3.LOAD: 指定可装入段,通过p_filesz和p_memsz进行描述。文件中的字节会映射到内存段的起始位置。
4.DYNAMIC: 指定动态链接信息。
5.GNU_RELRO: 指定在重定位结束之后那些内存区域是需要设置只读。
5.3.4 hello.elf没有hello.elf的重定位节
图 5-5 无重定位节
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
如图5-6,ELF文件从概念上来说包括了5个部分:
1.ELF header,描述体系结构和操作系统等基本信息,指出section header table和program header table在文件的位置
2.program header table,这个是从运行的角度来看ELF文件的,主要给出了各个segment的信息,在汇编和链接过程中没用
3.section header table,这个保存了所有的section的信息,这是从编译和链接的角度来看ELF文件的
4.sections,就是各个节区
5.segments,就是在运行时的各个段
图 5-6 ELF文件结构
图 5-7-1 edb查看内存单元
图 5-7-2 edb查看内存单元
5.5 链接的重定位过程分析
分析hello与hello.o的不同,说明链接的过程。结合hello.o的重定位项目,分析hello中对其怎么重定位的。
命令:objdump -d -r hello > hello01.asm
图 5-8 hello的反汇编指令
图 5-9 hello的反汇编代码
(1)链接后函数数量增加
链接后,程序调用的外部函数被链接进入可执行文件,如链接后的反汇编文件hello01.asm中,多了.plt,.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数。因为动态链接器将共享库hello.c用到的函数加入到了elf中。
(2)hello的反汇编结果比hello.o的多出来很多节,例如.init节和.plt节。
(3)关于重定位
hello.o中的待重定位条目在hello中被重定位,机器指令的操作数经过运算被修改,使得程序可以直接访问相应的函数与数据。例如,程序中的puts(这个是printf被优化成puts的),在libc中,链接器首先会确定puts函数在libc中的地址,然后将这个地址写入到hello.o中对puts的调用指令中,然后更新符号表,使得符号puts在最后的可执行文件中指向libc中的实际地址,最后根据重定位表,修改hello.o中对puts的所有引用,使他们指向正确的内存地址。
(4)hello.o中main的地址从0开始,hello中main的地址不再是0,因为库函数的代码都链接到了程序中。
5.6 hello的执行流程
使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。
执行过程大致如图5-10所示
图 5-10 edb执行hello过程
名称 地址
ld-2.27.so! dl_start <0x00000000000e08e8>
ld-2.27.so! dl_init <0xf00000000000706e8>
ld-2.27.so!_dl_allocate_tls_init <0x00007f9fddc80770>
ld-2.27.so!_dl_allocate_tls <0x00007f9fddc809a0>
ld-2.27.so!_dl_deallocate_tls <0x00007f9fddc80a10>
ld-2.27.so!_dl_make_stack_executable <0x00007f9fddc81130>
ld-2.27.so!_dl_find_dso_for_object <0x00007f9fddc81480>
ld-2.27.so!_dl_exception_create <0x00007f9fddc84ca0>
ld-2.27.so!_dl_exception_create_format <0x00007f9fddc84da0>
ld-2.31.so!_dl_exception_free <0x00007f9fddc85250>
ld-2.27.so!__tunable_get_val <0x00007f9fddc865d0>
ld-2.27.so!__tls_get_addr <0x00007f9fddc86da0>
ld-2.27.so!__get_cpu_features <0x00007f9fddc86df0>
ld-2.27.so!malloc <0x00007f9fddc89490>
ld-2.27.so!calloc <0x00007f9fddc895b0>
ld-2.31.so!free <0x00007f9fddc895f0>
ld-2.27.so!realloc <0x00007f9fddc897e0>
ld-2.27.so!_dl_signal_exception <0x00007f9fddc89a70>
ld-2.27.so!_dl_signal_error <0x00007f9fddc89ac0>
ld-2.27.so!_dl_catch_exception <0x00007f9fddc89c40>
ld-2.27.so!_dl_catch_error <0x00007f9fddc89d30>
hello!_init <0x0000000000401000>
hello!puts@plt <0x0000000000401030>
hello!printf@plt <0x0000000000401040>
hello!getchar@plt <0x0000000000401050>
hello!atoi@plt <0x0000000000401060>
hello!exit@plt <0x0000000000401070>
hello!sleep@plt <0x0000000000401080>
hello!_start <0x00000000004010f0>
hello!_start <0x000000000x400550>
hello!_main <0x0000000000400582>
hello!puts@plt <0x00000000004004f0>
hello!exit@plt <0x0000000000400530>
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。
GOT表:
每一个外部定义的符号在全局偏移表中有相应的条目,GOT位于ELF的数据段中,叫做GOT段。其作用是把位置无关的地址计算重定位到一个绝对地址。程序首次调用某个库函数时,运行时连接编辑器(rtld)找到相应的符号,并将它重定位到GOT之后每次调用这个函数都会将控制权直接转向那个位置,而不再调用rtld。
PLT表:过程连接表(Procedure Linkage Table),一个PLT条目对应一个GOT条目。当main()函数开始时,会请求plt中这个函数的对应GOT地址,如果第一次调用那么GOT会重定位到plt,并向栈中压入一个偏移,程序的执行回到_init()函数,rtld得以调用就可以定位printf的符号地址,第二次运行程序再次调用这个函数时程序跳入plt,对应的GOT入口点就是真实的函数入口地址。
动态连接器并不会把动态库函数在编译的时候就包含到ELF文件中,仅仅是在这个ELF被加载的时候,才会把那些动态函库数代码加载进来,之前系统只会在ELF文件中的GOT中保留一个调用地址。
通过readelf工具,在hello的节头表中可以发现GOT表,如图5-11所示
图 5-11 got表
调用_dl_start之前为:
图 5-12-1
调用_dl_start之后为:
图 5-12-2
在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。
5.8 本章小结
本章详细讨论了链接的概念与作用、在Ubuntu下链接的命令,以及可执行目标文件hello的格式,详细分析可执行目标文件和可重定位目标文件的区别。解释了hello虚拟地址空间并分析了链接的重定位过程。通过hello的执行流程,深入探讨了hello的动态链接,涉及ELF的结构,重定位的实现与动态连接。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
进程是一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程的作用:
通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行。
功能:
实际上Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。不仅如此,Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。Shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果
处理流程:
shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序(这里的应用程序可以是linux本身的实用程序,如ls和rm,也可以是购买的商业程序,如xv,或者是自由软件,如emacs)。然后shell在搜索路径里寻找这些应用程序(搜索路径就是一个能找到可执行程序的目录列表)。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给linux内核。
6.3 Hello的fork进程创建过程
当shell运行hello时,父进程调用fork函数生成这个程序的子进程。这个子进程几乎与父进程相同:子进程得到与父进程相同的虚拟地址空间的副本,包括代码,数据段,堆,共享库以及用户栈。不同的是子进程与父进程的PID不同。fork()函数创建新进程的过程:
(1)给新进程分配一个标识符;
(2)在内核中分配一个PCB,将其挂在PCB表上;
(3)复制它的父进程的环境(PCB中大部分的内容);
(5)复制父进程地址空间里的内容(代码共享,数据写时拷贝);
(6)将进程置成就绪状态,并将其放入就绪队列,等待CPU调度。
6.4 Hello的execve过程
execve函数在新创建的子进程的上下文中加载并运行hello程序。execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回到调用程序。所以,execve调用一次且从不返回。
加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
在运行hello程序时,进程为应用程序提供了以下抽象:(1)一个独立的逻辑控制流,给人一种错觉,仿佛进程独占地使用处理器;(2)一个私有的地址空间,使我们的程序看起来像是独占地使用内存。
逻辑控制流:
当我们使用调试器逐步执行程序时,会看到一系列的程序计数器(PC)值,这些值唯一地对应于程序的可执行文件中的指令或动态链接的共享对象中的指令。这个PC值的序列称为逻辑控制流,或逻辑流。当一个逻辑流与另一个流在时间上重叠执行时,这些流被称为并发流,并且它们是并发运行的。
上下文切换:
操作系统内核通过一种称为上下文切换的高级异常控制流机制来实现多任务处理。内核为每个进程维护一个上下文,上下文是内核重新启动被抢占的进程所需的状态。
时间片:
进程在每一时间段内执行其控制流的一部分,这个时间段称为时间片。因此多任务处理也称为时间分片。
用户模式和内核模式:
处理器通常通过控制寄存器中的模式位提供这种功能。当模式位设置时,进程运行在内核模式下,此时进程可以执行所有指令,并访问系统中的任何内存位置。当模式位未设置时,进程运行在用户模式下,此时进程不能执行特权指令,也不能直接访问内核区域的代码和数据。
上下文信息:
上下文是内核重新启动一个被抢占的进程所需的状态信息,包括通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构的值。在hello程序执行过程中,当进程调用 execve 函数时,会为hello程序分配新的虚拟地址空间。程序开始在用户模式下运行,调用 printf 函数输出“Hello 2022111915 fjw 13009730974”。随后,程序调用 sleep 函数,进程进入内核模式,运行信号处理程序,之后返回用户模式。在执行过程中,CPU不断进行上下文切换,将执行过程划分为多个时间片,与其他进程交替使用CPU,从而实现进程调度。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
hello执行过程中出现的异常种类可能会有:中断、陷阱、故障、终止等。
中断:中断是来自处理器外部的I/O设备的信号的结果。如在按下ctrl-z后,会触发一个中断异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
故障:故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则处理程序返回到内核中的abort例程并将其终止。
终止:终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。
(1)如图6-1,正常输入,循环打印10次后输入一个字符正常退出;
图 6-1
(2)如图6-2,运行过程中不停乱按键盘,包括回车对进程无影响,但是会将输入的内容存到缓冲区,作为接下来的命令行输入。
图 6-2
(3)Ctrl+C,如图6-3,会使内核发送一个SIGINT信号,信号处理程序会回收子进程
图 6-3
(4)Ctrl+Z,如图6-4,Shell收到sigstp信号,shell显示屏幕提示信息并挂起hello进程。
图 6-4
(5)如图6-5,图6-6对hello进程的挂起可以由ps和jobs命令查看,可以发现hello进程是被挂起了,但是没有回收。
图 6-5
图 6-6
(6)如图6-7,在shell中输入pstree命令,可以将所有进程以树状图显示:
图 6-7
(7)如图6-8,输入fg ,则命令将hello进程再次调到前台执行,可以发现shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下语句,然后正常结束,并被进程管理器完成进程回收
图 6-8
(8)如图6-9,输入kill 可以杀掉指定进程
图 6-9
6.7本章小结
本章阐述了进程的定义和作用,shell的作用和处理流程,执行hello时的fork和execve过程。分析了hello的进程执行和异常与信号处理过程。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:机器语言指令中,用来指定一个操作数或者是一条指令的地址,由CPU所生成,被内部和编程使用、并不唯一。例如程序中jump指令的地址。
线性地址:由虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。程序代码会产生逻辑地址,或者说段中的偏移地址,加上相应段基址就成了一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换产生物理地址。若是没有采用分页机制,那么线性地址就是物理地址。在程序中,这可以被看成是一种描述,描述hello程序应该在内存的哪些块上运行。
虚拟地址:由程序产生的由段选择符和段内偏移地址组成的地址。这2部分组成的地址并不能直接访问物理内存,而是要通过分段地址的变化处理后才会对应到相应的物理内存地址。即hello里的虚拟内存地址。
物理地址:程序运行时加载到内存地址寄存器中的地址,内存单元的硬件上的地址。物理地址用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应,物理地址是明确的、最终用在总线上的编号,不必转换,不必分页,也没有特权级检查。在hello程序中,物理地址表示程序运行时具体一条指令在内存地址上具体哪一块运行。
7.2 Intel逻辑地址到线性地址的变换-段式管理
1.基本原理:
在段式存储管理中,将程序的地址空间划分为若干个段(segment),这样每个进程有一个二维的地址空间。在段式存储管理系统中,为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为所有段分配其所需内存,这些段不必连续,物理内存的管理采用动态分区的管理方法。在为某个段分配物理内存时,可以采用首先适配法、下次适配法、最佳适配法等方法。在回收某个段所占用的空间时,要注意将收回的空间与其相邻的空间合并。段式存储管理也需要硬件支持,实现逻辑地址到物理地址的映射。
(1)可以分别编写和编译
(2)可以针对不同类型的段采取不同的保护
(3)可以按段为单位来进行共享,包括通过动态链接进行代码共享
这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。
总的来说,段式存储管理的优点是:没有内碎片,外碎片可以通过内存紧缩来消除;便于实现内存共享。缺点与页式存储管理的缺点相同,进程必须全部装入内存。
为了实现段式管理,操作系统需要如下的数据结构来实现进程的地址空间到物理内存空间的映射,并跟踪物理内存的使用情况,以便在装入新的段的时候,合理地分配内存空间。
(1)进程段表:描述组成进程地址空间的各段,可以是指向系统段表中表项的索引。每段有段基址(baseaddress),即段内地址。
在系统中为每个进程建立一张段映射表,如图7-1:
图 7-1
(3)空闲段表:内存中所有空闲段,可以结合到系统段表中。
图 7-2
在段式 管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址。这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般被称作段表地址寄存器。
7.3 Hello的线性地址到物理地址的变换-页式管理
1.基本原理
将程序的逻辑地址空间划分为固定大小的页(page),而物理内存划分为同样大小的页框(page frame)。程序加载时,可将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分配。该方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。在页式存储管理方式中地址结构由两部构成,前一部分是虚拟页号(VPN),后一部分为虚拟页偏移量(VPO):
图 7-3
页式管理方式的优点是:
(1)没有外碎片
(2)一个程序不必连续存放。
(3)便于改变程序占用空间的大小(主要指随着程序运行,动态生成的数据增多,所要求的地址空间相应增长)。
缺点是:要求程序全部装入内存,没有足够的内存,程序就不能执行。
在页式系统中进程建立时,操作系统为进程中所有的页分配页框。当进程撤销时收回所有分配给它的页框。在程序的运行期间,如果允许进程动态地申请空间,操作系统还要为进程申请的空间分配物理页框。操作系统为了完成这些功能,必须记录系统内存中实际的页框使用情况。操作系统还要在进程切换时,正确地切换两个不同的进程地址空间到物理内存空间的映射。这就要求操作系统要记录每个进程页表的相关信息。为了完成上述的功能,—个页式系统中,一般要采用如下的数据结构。
页表:页表将虚拟内存映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。页表是一个页表条目(PTE)的数组。虚拟地址空间的每个页在页表中一个固定偏移量处都有一个PTE。假设每个PTE是由一个有效位和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。
图 7-4 页表
MMU利用VPN来选择适当的PTE,将列表条目中PPN和虚拟地址中的VPO串联起来,就得到相应的物理地址。
图 7-5
7.4 TLB与四级页表支持下的VA到PA的变换
虚拟地址被划分成4个VPN和1个VPO。每个VPNi都是一个到第i级页表的索引,其中1<=i<=4.第j级页表中的每个PTE,1<=j<=3,都指向第j+1级的某个页表的基址。第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问4个PTE。将得到的PPN和虚拟地址中的VPO串联起来,就得到相应的物理地址。
图 7-6
7.5 三级Cache支持下的物理内存访问
L1 d-cache的结构如图所示:通过6-11位的组索引找到对应的组,将组中每一行的tag与CT比较,若标记位匹配且有效位为1,说明命中,根据0-5位的块偏移取出数据,如果没有匹配成功,则向下一级缓存中查找数据。取回数据后,如果有空闲块则放置在空闲块中,否则根据替换策略选择牺牲块。
图 7-7 Cache分块
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的pid。为了给这个新进程创建虚拟内存。它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。
7.7 hello进程execve时的内存映射
加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。
图 7-8 地址空间分配
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。例如:CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在主存中这一事实。缺页之前:
图 7-9
接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在VP3已经缓存在主存中了,那么也命中也能由地址翻译硬件正常处理了。缺页之后:
图 7-10
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
(1)隐式空闲链表
图 7-11 隐式空闲链表
空闲块通过头部中的大小字段隐含地连接着。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。
1)放置策略:首次适配、下一次适配、最佳适配。
首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配从上一次查询结束的地方开始。最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。
立即合并就是在每次一个块被释放时,就合并所有的相邻块;推迟合并就是等到某个稍晚的时候再合并空闲块。
带边界标记的合并:
图 7-12 合并
在每个块的结尾添加一个脚部,分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,从而使得对前面块的合并能够在常数时间之内进行。
(2)显式空闲链表
图 7-13 显式空闲链表
每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。使用双向链表使首次适配的时间减少到空闲块数量的线性时间。
空闲链表中块的排序策略:一种是用后进先出的顺序维护链表,将新释放的块放置在链表的开始处,另一种方法是按照地址顺序来维护链表,链表中每个块的地址都小于它后继的地址。
分离存储:维护多个空闲链表,每个链表中的块有大致相等的大小。将所有可能的块大小分成一些等价类,也叫做大小类。
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core i7在指定环境下介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射、hello进程、execve时的内存映射、缺页故障与缺页中断处理。同时简单阐述了堆的结构以及malloc和free时对堆的管理,以及利用gdb调试的方法进入printf函数内部查看printf对堆的操作。
(第7章 2分)
第8章 hello的IO管理
8.1 linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章1分)
结论
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
Hello程序在计算机中从出生到死亡整个过程:
2.预处理:生成hello.i文件;
4.汇编:将汇编语言转换成机器指令并生成重定位信息,结果为可重定位.o文件;
5.链接:与库链接,生成可执行文件hello;
6.创建进程:使用shell输入命令运行hello程序,并通过fork函数为hello创建进程;
7.加载程序:使用加载器,调用execve函数删除原来的进程内容,加载hello程序的代码和数据等内容到内存中;
8.执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流;
9.异常(信号):程序执行时,如果从键盘输入Ctrl-C等命令,会给进程发送一个信号,然后转到信号处理函数处理信号;
10.结束:程序执行完后,父进程回收子进程,内核删除为这个进程创建的所有数据结构。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i 预处理后得到的文本文件
hello.o 汇编后得到的可重定位目标文件
hello.elf hello.o的elf文件
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
[7] 深入理解计算机系统第三版.
[8] https://blog.csdn.net/fndfnd/article/details/85381044.
(参考文献0分,缺失 -1分)
Ongwu博客 版权声明:以上内容未经允许不得转载!授权事宜或对内容有异议或投诉,请联系站长,将尽快回复您,谢谢合作!