HELLO程序人生

摘要

本文详细介绍了“程序人生-Hello’s P2P”项目的全过程,从源代码的编写到程序的最终执行。项目涵盖了预处理、编译、汇编、链接、进程管理、存储管理和I/O管理等关键环节,旨在深入理解计算机系统的工作原理。
关键词:计算机系统,P2P,020

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

(1)P2P:Hello的诞生:程序员利用编辑器(Editor)编写文本文件hello.c,即一个程序(Process),经过预处理器(cpp),将以字符#开头的命令,修改原始C程序,生成以.i为文件扩展名的C程序,然后经过编译器(ccl),生成汇编代码,保存在以.s为文件扩展名的文本文件,再经过汇编器(as),生成可重定位目标程序,该程序是二进制文件,最后经过链接器(ld)生成二进制可执行目标文件,其整个流程如图1-1所示。

图1-1 编译系统
图1-1 编译系统

生成可执行文件hello后,shell读取用户命令./hello,通过fork创建一个子进程,执行hello程序。Hello实现从程序(Program)到进程(Process)。

(2)020:shell通过fork函数创建子进程execve映射虚拟内存,进入程序入口后载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流,当结束后,shell父进程负责回收hello进程,内核把它从系统中清除。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:64位Intel处理器、16GB RAM、1T SSD
软件环境:Windows 11,VMware(Ubuntu-20.04.4,64位)
开发工具:vi/vim/gcc

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.i

预处理得到的文本文件

hello.s

编译后得到的汇编文件

hello.o

汇编后得到的可重定位目标文件

hello

链接后得到的可执行文件

hello.dump

可执行文件的反汇编文件

hello.odump

可重定位目标文件的反汇编文件

hello.elf

hello的ELF格式文件

hello.oelf

hello.o的ELF格式文件

1.4 本章小结

本章主要介绍了hello程序的P2P(From Program to Process) 及020(From Zero-0 to Zero-0)的过程,以及此次大作业完成的硬件环境、软件环境以及开发工具,并列出了为完成本次大作业,生成的中间结果文件的名字以及文件的作用。

第2章 预处理

2.1 预处理的概念与作用

(1)概念:

编译预处理是编译过程中的一个阶段,它发生在源代码被编译成目标代码之前,用于对源程序进行修改,为编译做准备。

(2)作用:

1)文件包括:预处理器首先根据#include指令,将其引用的文件直接插入到源程序文本文件中,替代该命令行,生成扩展后的源程序。这样可以将常用代码和宏定义写在一个文件中,并在编写程序时引用该文件,提高写作效率并减少出错可能。

2)宏定义:在 C 语言源程序中允许用一个标识符来表示一个字符串,称为“宏”。被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。

3)条件编译:可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。

2.2在Ubuntu下预处理的命令

预处理命令:gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析

经过预处理后,hello.c文件中的注释消失,并且插入了大量文本,如图2-1所示。其中文本的来源即#include指定的头文件,如图2-2所示。hello.i也包含了一些其他头文件,如图2-3所示,这些头文件是被递归引用的。

图2-1 hello.i与hello.c的内容对比

图2-2 预处理插入的头文件stdlib.h以及unistd.h

图2-3被递归引用的头文件

2.4 本章小结

本章主要介绍了预处理的概念以及预处理的作用与功能,包括进行宏替换、处理文件,进行条件编译,在ubuntu下将hello.c文件预处理生成了hello.i文件,在此基础上分析了hello.i文件与源程序hello.c文件的相同与不同之处。发现预处理的过程会保留注释与#开头语句之外的部分,删除注释部分,同时将头文件与头文件所包含的文件直接插入到代码中。

第3章 编译

3.1 编译的概念与作用

(1)概念:

编译是指将便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序。在这里指编译器(cc1)将文本文件hello.i翻译成汇编语言程序hello.s的过程。

(2)作用:

编译的作用是将高级计算机语言所写作的源代码程序翻译为汇编语言程序,在这个过程中,会进行以词法分析、语法分析、语义分析来生成汇编语言程序,且编译器可能在这个过程中根据编译选项对程序进行一些适当的优化。       

3.2 在Ubuntu下编译的命令

编译命令:gcc -S hello.i -o hello.s,如图3-1所示。

图3-1 编译展示

3.3 Hello的编译结果解析

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析

3.3.1类型分析

(1)整型常量:当作立即数处理,例如5在编译后表示为$5。

(2)整型变量:作为局部变量,保存在通用目的寄存器(整数寄存器)或栈中,在示例中的argc的值就保存栈中,其在内存中的位置是-20(%rbp),如图3-3所示。当用寄存器保存时,其可以看作是寄存器的别名,节省从内存中读取数据的时间。

(3)字符串常量:将字符串存入内存中的.rotata常量区中,如图3-2所示。

图3-2字符串的编译

3.3.2操作分析

    (1)判断语句:使用cmp指令,根据比较的结果,条件码寄存器中的条件码将被恰当地设置。在图3-3的示例中,使用cmpl比较双字,即内存值-20(%rbp)的低32位与立即数$5比较。如果-20(%rbp)等于5,则条件码ZF置1;如果-20(%rbp)小于5,则条件码SF置1.

    (2)跳转语句:C源程序中的分支结构和循环结构在编译为汇编代码后,用跳转结构实现。在图3-3的示例中,je表示相等时跳转,跳转条件为ZF=1.

图3-3汇编代码与源代码对比

(3)函数调用:使用“过程”对代码进行封装和抽象。在示例中,如果argc不等于5,会进行两次过程调用,如图3-4所示。call语句会将PC(程序计数器,保存下一条指令代码在内存中的起始地址,不妨命名为A)设置为put@PLT的起始地址,并将返回地址(call指令的下一条指令的地址,即29 movl指令代码在内存中的地址)压入栈中。call exit@PLT同理。在图3-4的示例中,%rdi应该保存着向put@PLT转递的参数,%rip应该是字符串的首地址。

图3-4 过程调用

       此后,当被调用的过程结束后,通过ret指令从栈中弹出地址A,并将PC置为A,汇编代码如图3-5所示。

图3-5 ret指令

(4)循环语句:for循环的条件判断在.L3代码段中的cmpl $9, -4(%rbp)实现,-4(%rbp)可能是局部变量i的值,如果-4(%rbp)小于9(此时条件码为SF=1或者ZF=0),则跳转到代码段.L4,实现循环操作。每次循环,语句56将i的值加1. 传入参数char *argv[]存放在栈中,其中首地址存放在栈中-32(%rbp)的位置,argv[1]地址为-32(%rbp)+$8,argv[2]地址为-32(%rbp)+$16,argv[3]地址为-32(%rbp)+$24

图3-6 循环语句源程序与汇编程序对比

3.4 本章小结

本章主要介绍了编译的的概念以及编译的作用与功能,包括将高级语言指令翻译为汇编语言指令,在Ubuntu下将hello.i文件编译生成了hello.s文件。按照C语言的不同数据与操作类型,分析了源程序hello.c文件中的语句是怎样转化为hello.s文件中的语句的。其中数据类型包括数字常量、字符串常量和局部变量;操作类型包括算术操作、关系操作、数组\指针\结构操作控制转移以及函数调用。

第4章 汇编

4.1 汇编的概念与作用

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

(1)概念:指将汇编语言程序经过汇编器(as)转化为二进制的机器语言指令,并把这些指令打包成可重定位目标程序的格式,保存在目标文件.o中

(2)作用:把汇编语言翻译成机器语言,用二进制码0、1代替汇编语言中的符号,即让它成为机器可以直接识别的程序。最后把这些指令打包成可重定位目标程序的格式,并保存在目标文件.o中。

4.2 在Ubuntu下汇编的命令

汇编命令:gcc -c hello.s -o hello.o,如图4-1所示。

4-1 汇编过程展示

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析

ELF 全称 “Executable and Linkable Format”。使用语句readelf -a hello.o > hello.oelf生成hello.oelf文件,如图4-2所示。

图4-2 生成ELF

       (1)ELF头:以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,包括ELF头的大小(64字节)、目标文件的类型(可重定位文件)、处理器体系结构(AMD X86-64)、节头部表的文件偏移,以及节头部表中条目的大小(64字节)和数量(14),如图4-3所示。

图4-3 ELF目标文件格式

(2)节头部表:节头部表描述了目标文件中不同节的类型、地址、大小、偏移等信息,以及可以对各部分进行的操作权限,如图4-4所示。

图4-4 节头部表

(3)重定位节:包含两个部分:.rela.text节与.rela.eh_frame节

图4-5 重定位节

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

实验中使用objdump -d -r hello.o > hello.odomp将反汇编代码保存在hello.odomp中方便查看。

与直接编译得到的汇编代码相比,反汇编得到的代码有诸多不同,如图4-6所示:

(a)反汇编得到的汇编代码每一条语句都对应着一条机器指令,该机器指令显示为16进制数的形式,并且每条指令都有着一条对应的地址此外,反汇编的代码还包含有重定位条目,例如R_X86_64_PC32重定位一个使用32位PC相对地址的引用。

(b)反汇编代码中,立即数是16进制,而编译得到的汇编代码中立即数是10进制。

(c)分支跳转与(e)函数调用中,hello.odomp采用相对寻址的形式指明地址,而hello.o指明段的伪指令标号。

(d)反汇编代码中使用加载有效地址,而hello.s中写出伪指令。

(a)hello.odomp含机器代码及其地址

(b)hello.odomp与hello.s的立即数进制不同

(c)分支跳转语句不同

(d)字符串表示形式不同

(e)函数调用的不同

图4-6 hello.odomp与hello.s代码对比

4.5 本章小结

本章主要介绍了汇编的的概念以及汇编的作用,主要是将汇编语言程序经过编译器(as)转化为二进制的机器语言指令,并把这些指令打包成可重定位目标程序的格式,并保存在目标文件.o中,成为机器可以识别的程序。本章在Ubuntu下将hello.s文件经过汇编器(as)生成了hello.o文件,并分析了hello.o的ELF格式,用readelf等列出了其各节的基本信息,包括ELF头、节头部表、重定位节,然后对hello.o二进制文件进行反汇编,得到了反汇编程序代码hello.odomp,并分析了该反汇编程序与汇编语言程序hello.s中语句的对应关系。从数字进制、字符串常量的引用、分支转移以及函数调用的不同四个方面分析了二者的关系。应注意,本章汇编命令选项中是小写的c不是大写的C.

5章 链接

5.1 链接的概念与作用

注意:这儿的链接是指从 hello.o 到hello生成过程。

    (1)概念:链接(linking)是指将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。

    (2)作用:链接的作用是将预编译好了的一个目标文件(hello.o)或若干目标文件外加链接库合并成为一个可执行目标文件(hello)。链接使得分离编译称为可能,不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。当改变这些模块中的一个时,只需简单重新编译它并重新链接即可,不必重新编译其他文件。

5.2 在Ubuntu下链接的命令

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

链接命令: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/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello,如图5-1所示。

注:提行表示一个空格。

图5-1 链接过程

5.3 可执行目标文件hello的格式

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。各段的基本信息如图5-2所示。

图5-2 各段的基本信息

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

本进程的虚拟地址空间如图5-3所示,起始地址后6位为401000,结束地址后6位为402000,对应5.3中图5-2所示的.init到.rodata节。

图5-3 edb加载hello的虚拟地址空间

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。结合hello.o的重定位项目,分析hello中对其怎么重定位的。

链接后,代码长度增加,链接后插入了静态库中的相关代码。链接器将所有相同类型的节合并为同一类型的新的聚合节,并为每个节分配运行时的虚拟内存地址,然后修改字符串、函数调用等地址。首先,可重定位目标文件与可执行目标文件是二进制文件,如图5-4所示。反汇编后的文件包含用16进制数表示的机器代码,第i+1条指令的地址与第i条指令的地址的差值是第i条指令的字节数。

图5-4 反汇编文件代码分析

如图5-5所示,main函数引用了.rodata段的代码(可能是字符串),在左侧可重定位目标文件中,汇编器生成了一个使用32位PC相对地址的引用,lea指令开始于节偏移0x19的地方,包括3字节的操作码0x488d05,后面紧跟着对目标的32位PC相对引用的占位符。相应的重定位条目r的字段为:

r.offset

0x1c

r.symbol

.rodata

r.type

R_X86_64_PC32

r.addend

-4

图5-5 PC相对地址引用

该重定位条目告诉链接器修改开始于偏移量为0x1c处的32位PC相对地址引用,可以看出0x1c – 0x19 = 3,恰好为操作符lea机器代码占的字节数3。链接器确定当前节的地址ADDR(s) = ADDR(.text) = 4011d6,然后计算出引用的运行时地址refaddr = ADDR(s) + 0x1c = 0x4011d6 + 0x1c ,并确定ADDR(r.symbol),最后根据计算结果修改引用。

5.6 hello的执行流程

使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。

_init

0x401000

__gmon_start__@Base

0x403ff8

.plt

0x 401020

puts@plt

0x401090

printf@plt

0x4010a0

getchar@plt

0x4010b0

atoi@plt

0x4010c0

exit@plt

0x4010d0

sleep@plt

0x4010e0

_start

0x4010f0

main

0x4011d6

_fini

0x40127c

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。

动态链接器使用PLT和GOT实现函数动态链接,在dl_init调用之前,GOT存放的是PLT中函数调用的下一条指令;在dl_init调用之后,GOT指向重定位表用来确定调用函数地址.

图5-6 GOT的起始位置

图5-7 调用dl_init前的情况

图5-8 调用dl_init后的情况

5.8 本章小结

本章首先介绍了链接的概念和作用,并将hello.o通过指令转变为hello可执行程序,通过objdump、edb等工具详细分析了hello和hello.o的差别,以及链接中重定位和动态链接的过程。

6章 hello进程管理

6.1 进程的概念与作用

(1)概念:进程就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器地内容、程序计数器、环境变量以及打开文件描述符的集合。

(2)作用:进程提供独立的逻辑控制流,好像我们的程序独占地使用处理器;也提供一个私有的地址空间,好像我们的程序独占地使用内存系。

6.2 简述壳Shell-bash的作用与处理流程

shell为用户提供命令行界面,使用户可以在这个界面中输入shell命令,然后shell执行一系列的读/求值步骤,读步骤读取用户的输入的命令行,求值步骤则解析命令行,并运行程序。完成后重复上述步骤,直到用户退出shell,从而完成用户与计算机的交互来操作计算机。

shell打印一个命令行提示符,等待用户输入指令。在用户输入指令后,从终端读取该命令并进行解析,若该命令为shell的内置命令,则立即执行该命令;若不是内置命令,是一个可执行目标文件,则shell创建会通过fork创建一个子进程,并通过execve加载并运行该可执行目标文件,用waitpid命令等待执行结束后对其进行回收,从内核中将其删除;若将该文件转到后台运行,则shell返回到循环的顶部,等待下一个命令行。完成上述过程后,shell重复上述过程,直到用户退出shell。

6.3 Hello的fork进程创建过程

在Linux命令行中,shell父进程通过调用fork()函数可以创建一个新的运行的子进程。输入命令为:./hello 2022112928 oyzy 1。由于输入的是一个可执行文件,所以shell调用fork()函数后,新创建的子进程,该子进程几乎但不完全与父进程相同。

子进程得到与父进程虚拟地址空间相同的但独立的一份副本,包括代码、数据段、堆、共享库以及用户栈。子进程获得与父进程任何打开文件描述符相同的副本,意味着当父进程调用fork()函数时,子进程可以读写父进程中打开的任何文件。子进程有不同于父进程的PID,fork()被调用一次,返回两次。子进程返回0,父进程返回子进程的PID。

在shell创建子进程后会使用execve()函数来调用加载器,加载器是一个驻留在存储器中的操作系统代码。

6.4 Hello的execve过程

execve函数原型为:

int execve(const char *filename, const char *argv[], const char *envp[]);

execve首先加载文件filename,然后加载启动代码。该代码将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或者入口点来运行该程序。加载器会创建程序的内存影响,如图6-1所示。

图6-1 Linux x86-64运行时内存映像。没有展示出由于段对齐要求和地址空间布局随机化(ASLR)造成的空隙。区域大小不成比例。

在程序头部表的引导下,加载器将可执行文件的片(chunk)复制到代码段和数据段。然后加载器跳到程序的入口点,即_start函数(定义在系统目标文件ctrl.o中,对所有C程序一样)的地址。_start函数再调用系统启动函数__libc_start_main(定义在共享库libc.so中),初始化执行环境,调用用户层的main函数,处理main函数的返回值,并在需要的时候把控制权返回给内核。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

当开始运行hello时,内存为hello分配时间片(时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。如一个系统运行着多个进程,那么处理器的一个物理控制流就被分成了多个逻辑控制流,逻辑流的执行是交错的,它们轮流使用处理器,会存在并发执行的现象)然后在用户态下执行并保存上下文。如果在此期间内发生了异常或系统中断,则内核会休眠该进程,并在核心态中进行上下文切换,控制将交付给其他进程。

当hello 执行到 sleep时,hello 会休眠,再次上下文切换,控制交付给其他进程,一段时间后再次上下文切换,恢复hello在休眠前的上下文信息,控制权回到 hello 继续执行。

hello在循环后,程序调用 getchar() , hello 从用户态进入核心态,并再次上下文切换,控制交付给其他进程。最终,内核从其他进程回到 hello 进程,在return后进程结束。

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

(1)乱按:当在程序执行过程中随机乱按时,按下的字符串会直接显示,但不会干扰程序的运行。

图6-2 乱按时的程序运行

(2)Ctrl-C:按下Ctrl-C程序结束运行。从键盘输入Ctrl-C后,内核产生信号SIGINT,发送给hello的父进程,父进程收到它后,向子进程发生SIGKILL来强制终止子进程hello并回收它。这时再运行ps,可以发现并没有进程hello,可以说明它已经被终止并回收了。

图6-3 运行时按下Ctrl-C

(3)Ctrl-Z:按下Ctrl-Z产生中断异常,发送信号SIGSTP,这时hello的父进程shell会接收到信号SIGSTP并运行信号处理程序,将程序挂起。再按下ps和jobs指令,显示hello程序停止运行,如图6-4所示。

由图6-5,可以看到hello的父进程是bash。

图6-4 运行时按下Ctrl-Z

图6-5 pstree结构

图6-6 输入fg 指令恢复运行

图6-7 输入kill -9 PID杀死进程

6.7本章小结

本章主要介绍了进程的概念与作用,以及壳Shell-bash作用与处理流程,明确了hello的fork进程创建过程与execve过程, 分析了hello执行过程中按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等出现的异常、产生的信号,在Ctrl-z后运行ps、jobs、pstree、fg、kill等命令,查看这些指令对应的内容,进程的状态等,说明了异常与信号的处理。

7章 hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

(1)逻辑地址:逻辑地址为由程序产生的与段有关的偏移地址。逻辑地址分为两个部分,一个部分为段基址,另一个部分为段偏移量。在CPU保护模式下,需要经过寻址方式的计算和变换才可以得到内存中的有效地址。在hello的反汇编代码中的地址即为逻辑地址,需要加上相应的段基址才能得到真正的地址。

(2)线性地址:线性地址是地址空间中连续的整数,是逻辑地址到物理地址变换之间的中间层。在各段中,逻辑地址是段中的偏移地址,其偏移量加上基地址就是线性地址。可执行目标文件hello反汇编代码中的偏移地址(逻辑地址)与基地址相加后,即得到了对应内容的线性地址。

(3)虚拟地址:虚拟地址是指程序访问存储器所使用的逻辑地址,包含两部分:虚拟页号(VPN)+虚拟页偏移量(VPO)。使用虚拟寻址时,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送至内存前先转换成适当的物理地址。在Linux中,虚拟地址数值等于线性地址。在查看可执行目标文件hello的elf格式时,程序头中的VirtAddr即为各节的虚拟地址。由于在Linux中,虚拟地址数值等于线性地址,所以在hello反汇编代码中的地址加上对应段基地址的值即为虚拟地址。

(4)物理地址:计算机系统的主存被组织为一个由M个连续的字节大小的单元组成的数组,每字节都有一个唯一的物理地址(Physical Address, PA)。从第一个字节的地址为0开始,往后依次递增。在hello的运行过程中,hello内的虚拟地址经过地址翻译后得到的即为物理地址,并在机器中通过物理地址来访问数据。

7.2 Intel逻辑地址到线性地址的变换-段式管理

逻辑地址表示为[段标识符:段内偏移量],段标识符是一个16位长的字段(段选择符)。首先根据段选择符的T1选择,当前要转换段为是GDT或LDT,再根据寄存器,得到其地址,再拿出段选择符中前13位,在这个数组中,查找到对应的段描述符,就可以得到Base,然后将基地址与逻辑地址相加,获得线性地址。

7.3 Hello的线性地址到物理地址的变换-页式管理

hello的线性地址即hello的虚拟地址,通过页表实现向物理地址的翻译,如图7-1所示。运行时,处理器将hello进程相关的虚拟地址(VA)发送给内存管理单元(MMU),MMU将根据VA的前n-p位虚拟页号(VPN)作为页表的索引,在内存中的页表(PT)中定位页表条目(PTE),再将页表条目中的物理页号(PPN)与虚拟页偏移量(VPO)串联起来作为物理地址(PA),然后返回给MMU,得到PA的MMU在高速缓存/内存中读取对应数据,返回给处理器。

图7-1 地址翻译

7.4 TLB与四级页表支持下的VA到PA的变换

TLB(Translation lookaside buffer)是一个虚拟地址的缓存,每一行保存一个由单个PTE组成的块,其中用于组选择和行匹配的索引和标记字段是从虚拟地址(VA)中的VPN中提取。如图7-2所示,MMU(绿色部分)达到VA后,向TLB中寻找,而不再像7.3节中描述的在内存的页表中搜索。如果不命中,MMU将页表条目地址发送到高速缓存/内存,并在其中搜索PTE,替换TLB中的一行,继续执行图7-2(a)中的步骤4,即(b)中的步骤5.

图7-2 TLB在地址翻译中的作用
(a)命中,(b)不命中

对于四级页表,VPN被分为4个部分,分别为VPN1、VPN2、VPN3、VPN4,VPNi为第i级页表的索引,页表中的每个PTE都是下一级页表的基址,如图7-3所示。

图7-3 多极页表映射

7.5 三级Cache支持下的物理内存访问

得到物理地址后,根据物理地址从cache中寻找。到了L1后,寻找物理地址检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3,如果L3也不命中,则需要从内存中将对应的块取出放入cache中,直到出现命中。

7.6 hello进程fork时的内存映射

当fork函数被shell进程调用创建一个hello的进程时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将hello进程和shell进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

7.7 hello进程execve时的内存映射

Execve加载hello可执行目标文件,该步骤包括:删除已经存在的用户区域,映射私有区域,映射共享区域,设置程序计数器(PC)。

7.8 缺页故障与缺页中断处理

(1)缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。

(2)处理方式:

缺页处理程序是系统内核中的代码,选择一个牺牲页,如果这个牺牲页被修改过,那么就将它交换出去,换入新的页并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

动态内存分配器维护着一个进程的虚拟内存区域,称为堆,分配器将堆视为一组不同大小的块来维护,每个块就是一个个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都要求应用显式地分配块。显式分配器:应用显示分配与释放,例如malloc、free函数。隐式分配器:也叫做垃圾收集器,检测不再使用时,就释放这个快。

7.10本章小结

本章主要内容为内存管理。介绍了hello程序的存储空间,如何得到最终的地址,介绍了hello的四级页表的虚拟地址到物理地址的转换,三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

在Linux中,所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行。我们可以对文件的操作有:打开关闭操作open()和close();读写操作read()和write();改变当前文件位置lseek()等等。

8.2 简述Unix IO接口及其函数

(1)Unix IO接口:Linux将设备映射为文件,所有输入输出都当做文件读写来执行。Unix I/O接口使得所有的输入和输出都能以统一一致的方式来执行:

1)打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

2)每个进程开始时三个打开的文件:标准输入、标准输出和标准错误,描述符分别为0、1、2。头文件<unistd.h>定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。

3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为k。

4)读写文件:一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n。给定一个大小为m 字节的文件,当k>=m 时执行读操作会触发EOF (end of file) 条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号”。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k。

5)关闭文件:当一个应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

(2)Unix IO函数:

1)open()函数:用于打开或创建文件,在打开或创建文件时可以指定文件的属性以及用户的权限等各种类型的参数。函数原型:int open(const char *pathname,int flags,int perms);参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式;返回值:若成功,返回文件描述符;若失败,返回-1;

2)close()函数:用于关闭一个被打开的的文件;所需头文件:#include <unistd.h>;函数原型:int close(int fd);参数:fd文件描述符;函数返回值:若成功,返回0;若出错,返回-1。

3)read()函数:从文件读取数据;所需头文件:#include <unistd.h>;函数原型:ssize_t read(int fd, void *buf, size_t count);参数:fd:将要读取数据的文件描述词;buf:指缓冲区,即读取的数据会被放到这个缓冲区中去;count:表示调用一次read操作,应该读多少数量的字符;返回值:返回所读取的字节数;0(读到EOF);-1(出错)。

4)write()函数:向文件写入数据;所需头文件:#include <unistd.h>;函数原型:ssize_t write(int fd, void *buf, size_t count);返回值:写入文件的字节数(成功);-1(出错)。

5)lseek()函数:用于在指定的文件描述符中将将文件指针定位到相应位置;所需头文件:#include <unistd.h>,#include <sys/types.h>;函数原型:off_t lseek(int fd, off_t offset,int whence);参数:fd:文件描述符;offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移);返回值:若成功,返回当前位移;若失败,返回-1。

8.3 printf的实现分析

https://www.cnblogs.com/pianist/p/3315801.html

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

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;

}

8.4 getchar的实现分析

当程序调用getchar时,程序等待用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车(回车也在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要对Linux系统的输入输出进行了分析。在Linux系统中,所有输入输出设备都被看做是文件,通过打开、关闭、读、写等方式进行管理。本章还对printf函数和getchar函数的实现进行了分析。

结论

用计算机系统的语言,逐条总结hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

程序员编写hello程序,即hello.c源文件,通过预处理生成hello.i,再通过编译、汇编、链接,生成可执行文件hello。然后shell使用fork函数为其创建一个子进程,execve函数加载到内存中执行,完成虚拟内存映射,PC指向程序入口,实现P2P过程。程序运行时,会将虚拟内存翻译为物理内存在内存中读取数据。由于缓存的读取速度更快,在内存中读取数据时还会应用多级缓存进行物理地址的映射。待程序结束后,shell执行waitpid函数,内核回收程序,实现020过程。通过对CS的学习,我感受到了计算机系统设计的精妙,尤其注意到设计人员在面临一个问题时根据研究对象的特点所做出的解决方案,例如发明进程、虚拟内存等概念。在创新的过程中,应切实抓住问题的痛点,发挥想象力,实现新的设计。

附件

列出所有的中间产物的文件名,并予以说明其作用。

hello.i

预处理后得到的文件

hello.s

编译后得到的汇编代码

hello.o

汇编后得到的可重定位目标文件

hello

链接后得到的可执行文件

hello.dump

hello的反汇编代码文件

hello.odump

hello.o的反汇编代码文件

hello.elf

hello的ELF格式文件

hello.oelf

hello.o的ELF格式文件

参考文献

[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.

  • 12
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值