计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术学院
学 号 2022113536
班 级 22wl028
学 生 王欢
指 导 教 师 郑贵滨
计算机科学与技术学院
2024年5月
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
关键词:关键词1;关键词2;……;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第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 -
参考文献....................................................................................... - 16 -
第1章 概述
1.1 Hello简介
P2P:
首先,在一个磁盘文件里编写代码,得到一个源程序hello.c;调用C预处理器对该程序进行宏替换,得到hello.i文件;然后调用C编译器对hello.i文件进行处理,得到hello.s这个汇编语言文件;接着调用汇编器处理可重定向的目标文件hello.o文件;最后通过链接器链接,得到可执行文件。然后在Bash里启动这个程序,这时shell会调用fork函数创建一个进程,然后调用execve函数加载这个程序并运行。
O2O:
父进程调用fork函数创建一个子进程,为其创建相应的数据结构、创建虚拟内存并分配唯一的PID,然后调用execve函数将可执行程序进行内存映射并设置程序计数器。当程序退出后,由父进程或祖父进程来回收该进程,彻底删除掉它的痕迹。
1.2 环境与工具
1.2.1 硬件环境
11th Gen Intel(R) Core(TM) i7-1165G7 2.80 GHz
1.2.2 软件环境
Windows 23H2
开发工具
Visual Studio 2019 64位,CodeBlocks 64位,UBUNTU
1.3 中间结果
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行文件
hello:链接之后的可执行文件
hello_o.elf:hello.o的ELF格式
hello.elf:hello的ELF格式
hello_o.objdump:hello.o反汇编代码
hello.objdump:hello的反汇编代码
1.4 本章小结
本章主要介绍了hello.c的P2P与O2O,然后列举了本篇论文写作过程中对hello文件操作的所有需要的环境与工具,最后对实验过程中用到的所有中间文件及其作用和使用时期以一个表格的形式进行了大致的描述。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理是编译过程的前置步骤,是编译过程中的一个阶段,它是在实际编译源代码之前进行的一系列文本处理操作。通过预处理器对原始的C程序进行修改和处理。它可以实现宏替换、文件包含、条件编译、符号定义等功能,以提高代码的可读性和可维护性。
作用:它通过处理源代码中的预处理指令来优化代码结构、提高代码复用性和可读性,并为后续编译阶段提供一个更加清晰、易于处理的源代码文件。
1.头文件包含:预处理阶段会处理源代码中的#include指令,将这些指令所指定的头文件内容插入到源代码中相应的位置。这一步骤有助于组织和管理代码,使得源代码能够更方便地引用其他文件或模块中定义的函数、变量和类型。
2.宏定义:预处理会处理源代码中的#define指令,根据这些指令进行文本替换。宏定义可以用于定义常量、函数原型或更复杂的代码片段,从而提高代码的可读性和可维护性。通过宏定义,程序员可以定义一些通用的、可重用的代码片段,减少代码中的重复部分。
3.条件编译:预处理阶段还会处理源代码中的条件编译指令,如#if、#ifdef、#ifndef、#else、#elif和#endif等。这些指令允许程序员根据某些条件来决定是否编译源代码中的某些部分,从而实现代码的灵活性和可配置性。例如,可以根据不同的操作系统或平台来编译不同的代码段,或者根据调试需求来选择性地编译某些调试代码。
4.其他预处理指令:预处理阶段还支持其他预处理指令,如#line(用于控制编译器的错误和警告消息的行号信息)、#error(用于在编译时生成错误消息)和#pragma(用于为编译器提供特定的指令或选项)。
5.提高代码复用性和可读性:通过预处理,程序员可以定义一些通用的、可重用的代码片段(如宏定义),从而减少代码中的重复部分,提高代码复用性。同时,预处理还可以帮助组织和管理代码,使得代码结构更加清晰、易于阅读和维护。
6.优化编译过程:预处理阶段对源代码进行了一些文本替换和条件控制操作,生成了一个预处理后的源代码文件。这个预处理后的文件将作为后续编译阶段的输入。由于预处理阶段已经处理了一些重复的代码和条件编译的代码段,因此可以减少后续编译阶段的工作量,提高编译效率。
2.2在Ubuntu下预处理的命令
指令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
打开hello.i文件,可以看到main函数和全局变量的代码没有被改变,而原始的#include语句被替换成了大量头文件中的内容,对源文件中的宏进行了展开操作,包括外部函数的声明、数据结构的定义和数据类型的定义等。
同时,在预处理过程中删掉了源程序当中的注释内容。
但可以看出,与处理后的hello.i文件实质上还是一个c语言文件
2.4 本章小结
本章首先介绍了预处理的概念与作用,接着演示了hello.c文件如何进行预处理变为hello.i文件,并对与处理后的文件结果进行了分析。
第3章 编译
3.1 编译的概念与作用
概念:
编译过程是指编译器ccl通过词法分析、语法分析、语义分析和优化,将源程序文件 hello.i 转换为汇编程序文件 hello.s。在这个过程中,编译器将源程序中的每条合法指令翻译成等价的低级机器语言指令,并以文本的形式描述在 hello.s 中。
作用:
编译的作用是将用高级语言编写的源代码转换为机器代码、字节码或另一种编程语言。编译的过程需要通过词法分析、语法分析、语义分析和优化等步骤,检查源代码是否符合语法规则,并将其翻译成目标平台能够执行的格式。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
1. 定义字符串,包含特定的Unicode字符。
2.开辟栈,因为栈倒着生长,所以做减法操作,开辟空间大小为32bytes,main函数变量argc保存在-20(%rbp),argv*保存在-32(%rbp)中
3.比较-20(%rbp)(argc)是否等于4,如果等于的话则输出字符串,并exit。如果不等于4的话,跳转到.L2。
4..L2内容:把0存到-4(%rbp)(局部变量i),并跳转到L3
5.将-4(%rbp)(局部变量i)的内容与8作比较,若大于8,则调用getchar,并把0放到寄存器rax准备返回0(return 0)。若-4(%rbp)小于等于8,则跳转.L4
6.L4的首先把-32(%rbp)(argv*)放到寄存器rax,用rax+16,和rax+8得到argv[1],argv[2]的地址,再取用改地址取数(movq (%rax),%rdx),分别放到寄存器rdx和寄存器rax中,并调用pintf函数。
然后取-32(%rbp)+24的数为地址做取数操作(argv[3]),存到寄存器rax、rdi中,然后使用atoi函数。这里有一点非常值得关注!传入参数为字符串型,所以用寄存器rdi,返回参数为int型,所以用的是寄存器eax。这里完成了一次隐形类型转换
后面把eax的值放到寄存器edi,使用sleep函数,并给-4(%rbp)中的数(局部变量i)加一。
文件说明:.file 声明源文件,.text指示代码段,.section指示rodata段。
数据:定义字符串,包含特定的Unicode字符
赋值:使用数据传送指令mov,后面接b,w,l,q分别代表传送数据为8位,16位,32位,64位
算术操作:加用add,减用sub,均为第二个数op第一个数,存在第二个数的位置上,后面接b,w,l,q分别代表传送数据为8位,16位,32位,64位
数组/指针/结构操作:
数组:数组头部指针加偏移量作为地址,做取数操作
指针:指针作为地址,做一次取数操作
结构体:通过结构体内部的偏移量来访问。
控制转移:
1.If语句:该程序中使用cmp类比较指令将argc与4比较,如果不等于4,则跳转到L2执行。
For循环:该程序先使用MOV类数据转移指令将局部变量i赋值为0,没进行完依次循环后i+1,之后使用cmp类比较指令将i与9比较,小于等于9的话跳转到L4。
Puts: 将一个以空字符结尾的字符串发送到标准输出流(通常是屏幕)。该函数会在字符串的末尾自动添加一个换行符,使得下一次输出从新的一行开始
exit: 立即终止调用进程
printf:向屏幕输出
:atoi:把字符串转化为int
Sleep:实现程序的休眠
Getchar:从缓冲区读取字符
3.4 本章小结
1.汇编语言中的指令是对底层硬件操作的抽象表示,每条指令都有特定的操作码(opcode)和操作数(operand)。
2.汇编语言中的内存访问通过使用内存地址和偏移量来实现。可以使用寄存器或立即数作为地址和偏移量,以读取或写入内存中的数据。
3.汇编语言中的操作指令用于执行各种算术、逻辑和位操作。这些指令可以对寄存器和内存中的数据进行加减乘除、逻辑与或非、移位等操作。
4.在编写x86汇编程序时,需要了解所用的汇编器的语法和约定,并且要注意内存对齐、寄存器保存、函数调用等规范。
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编器as将汇编语言(这里是hello.s)翻译成机器语言(hello.o)的过程称为汇编,同时这个机器语言文件也是可重定位目标文件,是二进制文件。
作用:
汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位 目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
指令:gcc hello.s -c -o hello.o
4.3 可重定位目标elf格式
首先使用指令:readelf -a hello.o > hello_o.elf,得到hello_o.elf文件
ELF Header(ELF文件头):
1.Magic(魔数):用于标识ELF文件格式的特定字节序列。
2.Class(类别):指定了ELF文件的位数,这里是ELF64,表示64位的ELF文件。
3.Data(数据编码):指定了数据的编码方式,这里是2's complement和little endian,表示使用补码表示的小端字节序。
4.Version(版本):指定了ELF文件的版本,当前为版本1。
5.OS/ABI(操作系统/ABI):指定了目标操作系统和应用程序二进制接口,这里是UNIX - System V。
6.ABI Version(ABI版本):指定了应用程序二进制接口的版本,这里为0。
7.Type(文件类型):指定了ELF文件的类型,这里是REL(可重定位文件)。
8.Machine(目标机器):指定了目标处理器的体系结构,这里是Advanced Micro Devices X86-64(AMD的64位x86处理器)。
9.Entry point address(入口点地址):指定了程序的入口点地址,这里是0。
10.Start of program headers(程序头开始位置):指定了程序头表在文件中的偏移量,这里是0。
11.Start of section headers(节头开始位置):指定了节头表在文件中的偏移量,这里是1240字节。
12.Flags(标志):指定了与ELF文件相关的标志,这里是0。
13.Size of this header(ELF头的大小):指定了ELF头的大小,这里是64字节。
14.Size of program headers(程序头表的大小):指定了程序头表的大小,这里是0字节。
15.Number of program headers(程序头表的数量):指定了程序头表的数量,这里是0。
16.Size of section headers(节头表的大小):指定了节头表的大小,这里是64字节。
17.Number of section headers(节头表的数量):指定了节头表的数量,这里是14。
18.Section header string table index(节头字符串表索引):指定了节头字符串表在节头表中的索引,这里是13。
Section Headers(节头): 对各个节头的信息进行了列举,包括节的名称、类型、地址、偏移量、大小、标志。
1.text节,大小为0x92,类型为PROGBITS,标志为AX(只读可执行),偏移量为0X40,
2.rela.text节,大小为0xc0,类型为RELA,标志为I,偏移0x380字节
3.data节,已初始化的全局变量节,类型为PROGBITS,标志为WA(可读写),偏移0xd2
4.bss节,未初始化的全局和静态变量或初始化为0的全局或静态变量,大小为0,类型为PROGBITS,标志为WA,偏移0xd2
5.rodata节,只读数据,大小为0x33,标志为A(只读),偏移0xd8
6.comment节,版本控制信息,大小为0x24,标志为MS,偏移为0xef
7.note.GNU_stack节:标记可执行堆栈,大小为0x0字节,类型为PROGBITS,偏移量为0x12f
8.eh_frame节:处理异常,大小为0x38字节,类型为PROGBITS,偏移量为0x150,标志为A(表明该节的数据只读)。
9.rela.eh_frame节:.eh_frame节的重定位信息,大小为0x18字节,类型为RELA,偏移量为0x440,标志为I。
10.shstrtab节:包含节区名称,大小为0x61字节,类型为STRTAB,偏移量为0x188
11.symtab节:一个符号表,存放在程序中定义和引用的函数和全局变量的信息。大小为0x1b0字节,类型为SYMTAB,偏移量为0x338
12.strtab节:一个字符串表,包括.symtab和.debug节中的符号表,以及节头,偏移量为0x458字节
13shstrtab节:大小为0x74字节,也是一个字符串表,偏移量为0x458字节
Relocation section '.rela.text'(重定位节): 列举了重定位节中的条目,包括偏移量、信息、类型、符号值和符号名称。本程序中的重定位函数是:.L0,puts,exit,.L1,printf,atoi,sleep,getchar。
Relocation section '.rela.eh_frame'(重定位节): 列举了重定位节中的条目,包括偏移量、信息、类型、符号值和符号名称。
Symbol table '.symtab'(符号表): 列举了符号表中的条目,包括符号的值、大小、类型、绑定、可见性、索引和名称。
4.4 Hello.o的结果解析
使用指令:objdump -d -r hello.o > hello_o.objdump,查看文件
观察发现:该文件与hello.s文件大体相同,只有立即数的访问、分支转移、函数调用三点有所差异。
hello_o.objdump文件中的立即数存储格式均为0x开头,即十六进制,hello.s文件则是数字,说明是十进制;
hello.s文件是.L3这样的助记符,用户可以阅读,但电脑却无法执行;hello_o.objdump文件中则采用了相对寻址,通过偏移地址的方式来进行分支转移,电脑可阅读。;
在hello.s文件中,只需要call + 函数名@PLT就可表示调用函数;但在hello_o.objdump文件中则需写出它们的地址(此处地址都为0是因为还没有进行重定位)。
4.5 本章小结
本章主要介绍了汇编的过程,学习了ELF文件的内容以及格式,并分析了重定位前汇编程序和重定位后反汇编的差别,尽管他们看似大体相同,但已是两种截然不同的文件形式了。
第5章 链接
5.1 链接的概念与作用
概念:
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。
作用:
符号解析:链接器负责解析程序中使用的符号,即变量、函数或对象的名称。当程序中引用一个符号时,链接器会找到该符号的定义并建立符号与其定义之间的关联。
符号重定位:在链接过程中,目标文件或模块中的代码和数据可能会使用相对地址或符号引用来表示内存位置。链接器负责将这些相对地址转换为绝对地址,以确保程序在运行时可以正确访问内存中的代码和数据。
内存管理:链接器还负责将不同的目标文件或模块中的代码和数据组织在内存中的适当位置。它将代码段、数据段和堆栈等部分组合在一起,并分配适当的内存空间,以便程序可以正确地加载和执行。
库链接:链接器可以将外部库(如动态链接库或静态链接库)与程序进行链接。这样,程序可以使用库中提供的功能,而无需重新实现这些功能。
符号重复消除:如果程序中使用了多个目标文件或模块,并且它们都定义了相同的符号(如全局变量或函数),链接器将负责消除重复定义,以避免符号冲突和重复代码。
优化和代码生成:链接器可以进行一些优化操作,如代码压缩、死代码消除和代码重排等,以提高程序的性能和效率。
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
5.3 可执行目标文件hello的格式
使用指令:readelf -a hello > hello.elf,
- ELF头: 上次的节头数量为13个,这次变为27个,其它保持一致。
2.节头:对各个节头的信息进行了列举,包括节的名称、类型、地址、偏移量、大小、标志。
3程序头: 程序头部表描述了可执行文件的连续的片映射到连续的内存段的映射关系,描述了可执行文件在内存中的布局和加载信息。每个程序头部对应一个段,包含段的类型、在文件中的偏移、在内存中的虚拟地址、物理地址、大小和访问权限。
4. 动态段信息,包含了一些动态链接器需要的重要信息,如依赖的共享库、符号表、重定位信息
4.1共享库
4.2 重定位信息:在这里我们可以注意到.rela.text节消失,说明链接过程完成了重定位操作,但确切地址仍不确定,需动态连接后确定。
4.3符号表,内容有:
符号值(Symbol Value):符号的值或地址,表示符号在内存中的位置。
符号大小(Size):符号所占据的空间大小。
符号类型(Type):符号的类型,如函数、对象、未定义符号等。
符号绑定(Bind):符号的绑定类型,指示符号的可见性和链接性,如局部符号、全局符号、弱符号等。
符号可见性(Vis):表示符号可见性,是否可见。
符号的节的索引(Ndx):指示了符号所属的段或节的索引。
符号名称(Name):符号的名称,如函数名、变量名等。
5.4 hello的虚拟地址空间
运行指令:edb –-run hello
使用edb加载hello,可以看到进程的虚拟地址空间各段信息。可以看出,段的虚拟空间从0x400000开始,到0x400ff0结束。
.init段起始地址为0x401000,.text段的起始地址为0x4010f0,.rodata段的起始地址为0x402000,.data起始地址为0x404048,也都可以在edb中找到:
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
使用指令:objdump -d -r hello > hello.objdump
通过观察,可以发现,区别主要有以下几点:
1. hello.o中的汇编代码是从0开始的,而hello中的汇编代码已经使用虚拟内存地址来标记了,从0x400000开始。
2.hello.o中的相对偏移地址,在hello的汇编代码中已经全部计算出了其绝对地址。
链接过程:通过hello.o中跳转指令和call指令后为绝对地址,计算出在hello中重定位后的虚拟地址。
5.6 hello的执行流程
从加载hello到_start,到call main,以及程序终止的所有过程,其调用与跳转的各个子程序名和程序地址如下所示:
0000000000401000 <_init>
0000000000401090 <puts@plt>
00000000004010a0 <printf@plt>
00000000004010b0 <getchar@plt>
00000000004010c0 <atoi@plt>
00000000004010d0 <exit@plt>
00000000004010e0 <sleep@plt>
00000000004010f0 <_start>
0000000000401125 <main>
00000000004011c0 <__libc_csu_init>
0000000000401230 <__libc_csu_fini>
0000000000401238 <_fini>
5.7 Hello的动态链接分析
动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射,它使用偏移量表(GOT)和过程链接表(PLT)来实现函数的动态链接。所以在dl_init前后,.got文件会发生改变。查表知:.got文件地址为0x403ff0
5.8 本章小结
本章我们介绍了链接,链接是将多个目标文件组合成一个可执行文件或共享库的过程。静态链接将所有依赖项都包含在可执行文件中,而动态链接采用延迟加载策略,实现函数的动态链接。动态链接可以实现共享库的重用和节省内存,提高程序的可维护性和运行效率。
并且详细分析了hello.o是怎么链接成为一个可执行目标文件的过程,理解里面每个参数的变化过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念与作用
概念:
进程是执行中程序的实例。
作用:
在现代计算机中,进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序 一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行 我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。进程提供给应用程序两个关键抽象:一个独立的逻辑控制流;一个私有的地址空间。
6.2 简述壳Shell-bash的作用与处理流程
(1) 作用:
Shell是用户与操作系统之间完成交互式操作的一个接口程序,它为用户提供简化了的操作。
(2) 处理流程:将用户输入的命令行进行解析,分析是否是内置命令; 若是内置命令,直接执行;若不是内置命令,判断是否为可执行文件,若是则bash在初始子进程的上下文中加载和运行它;在前台或后台运行程序;接收SIGCHLD信号,回收子进程。
6.3 Hello的fork进程创建过程
根据shell的处理流程,可以推断,输入命令执行hello后,父进程如果判断不是内部指令,即会通过fork函数创建子进程。子进程与父进程近似,并得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈。父进程打开的文件,子进程也可读写。二者之间最大的不同或许在于PID的不同。Fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行新程序hello。函数原型为:int exeve(const char *filename, const char *argv[], const char *envp[]);如果成功,则不返回;如果错误,则返回-1。在execve加载了hello之后,它调用启动代码。启动代码设置栈,并将控制传递给hello的主函数(即main函数),该函数有以下原型:int main(int argv, char **argv, char **envp)或者等价的int main(int argc, char *argv[], char *envp)。
详细过程:删除已存在的用户区域。映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
时间片:
一个进程执行他的控制流的一部分的每一个时间段叫做时间片(time slice),多任务也叫时间分片(time slicing)。
进程的上下文切换:
调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被强占的进程。这种决策就叫调度(是由内核中的调度器的代码处理的)。
上下文切换:在内核调度了一个新的进程运行时,它就抢占当前进程,并使用一种上下文切换的机制来控制转移到新的进程:保存当前进程的上下文;恢复某个先前被强占的进程被保存的上下文;将控制传递给这个新恢复的进程;用户的上下文信息由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
用户态和内核态的转换:
为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
1.异常和信号异常:
中断-来自I/O设备的信号-异步-总是返回到下一条指令
陷阱-有意的异常-同步-总是返回到下一条指令
故障-潜在可恢复的错误-同步-可能返回到当前指令
终止-不可恢复的错误-同步-不会返回
2.执行中可能的异常:中断(Interrupt):中断是异步发生的,由处理器外部的I/O设备发送的信号引起。中断可以被用来处理各种事件,如时钟中断、输入/输出中断等。当发生中断时,操作系统会暂停当前的程序执行,并跳转到相应的中断处理程序。不同的中断对应不同的信号,例如时钟中断对应SIGALRM信号,键盘中断对应SIGINT信号。
陷阱(Trap):陷阱是有意的异常,是执行某条指令的结果。在Hello程序中,调用sleep函数会触发陷阱,操作系统会捕获该陷阱,并执行相应的处理操作。
故障(Fault):故障是由错误引起的异常,通常是一些可修正的错误。在Hello程序执行过程中,可能会出现缺页故障(Page Fault),系统发出SIGSEGV信号。操作系统会捕获该故障,并进行页面调度和加载,以满足程序的内存访问需求。
终止(Abort):终止是由不可恢复的致命错误引起的结果,通常是一些硬件错误。当发生终止时,操作系统会终止程序的执行,并向其发送相应的信号,如SIGSEGV信号(段错误)或SIGBUS信号(总线错误)。
- 键盘上的异常:
运行时乱按(包括回车):可以发现该方式并不会影响程序的正常运行,因为它们都会被最后的getchar函数读取,并覆盖掉。
按Ctrl-Z按键:我们发现该程序被挂起了,因为这样会发送SIGTSTP信号给前台进程组的每个进程,结果是停止前台作业。
可以通过jobs指令来查看当前的作业,可以看出当前的作业是hello进程,且状态是已停止。
使用ps命令可以查看当前所有进程以及它们的PID,进程包括bash,hello以及ps。
6.7本章小结
在本章中,我们学习了关于hello进程管理的相关概念和机制。我们了解了进程的概念,它是程序的执行实例,由操作系统进行管理以实现程序的正确执行;进程的创建过程,包括fork创建子进程和execve加载新程序,学习了进程的上下文切换,它是在多任务环境下实现进程调度和切换的重要机制。最后通过学习hello进程管理,我们对进程的创建、执行、上下文切换以及与异常和信号相关的处理有了更深入的了解。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。
线性地址:也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。
虚拟地址:也就是线性地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
索引号,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
这里面,我们只用关心Base字段,它描述了一个段的开始位置的线性地址。
全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
给定一个完整的逻辑地址段选择符+段内偏移地址,
看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
把Base + offset,就是要转换的线性地址了
7.3 Hello的线性地址到物理地址的变换-页式管理
在Hello程序中,线性地址首先被划分为页号和页内偏移量两部分。页号用于确定所在的页表项,而页内偏移量表示相对于页起始地址的偏移量。系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。通过页目录基址寄存器(PDBR)获取页目录表的起始地址。根据线性地址的高位部分(高10位),在页目录表中找到对应的页目录项(PDE),每个页目录项记录了一个页表的基址。使用页表基址和线性地址的中间部分(中间10位),在页表中找到对应的页表项(PTE),每个页表项包含了一个页框的物理基址。取得页表项中的页框物理基址,将其与线性地址的低位部分(低12位)进行组合,得到最终的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
当程序访问一个虚拟地址时,用MMU进行地址翻译时,会先将VPN传给TLB,看TLB中是否已经缓存了需要的PTE。CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配。如果存在,即TLB命中,系统可以直接从TLB中获取对应的物理地址,则得到 PPN(40bit)与 VPO(12bit)组合成 PA(52bit),这样省去了访问页表的开销,加快了地址变换速度。
如果TLB未命中,系统将按照四级页表的层次结构逐级访问页表,进行逐级的地址转换,直到找到对应的物理页框。首先,从线性地址中提取出四级页表的索引,根据索引在第一级页表(一级页目录)中找到对应的页表项,根据第一级页表项中记录的物理页表的基址,加上线性地址的中间部分,得到第二级页表的物理地址。未命中则依此查找第二级、第三级页表。
在这个过程中,每一级的页表都提供了下一级页表的物理地址。通过层层递进的方式,系统最终可以从四级页表中获取到对应虚拟地址的物理地址。
7.5 三级Cache支持下的物理内存访问
在具备三级缓存(Cache)支持的情况下,物理内存访问可以通过缓存来提高访问速度和效率。三级缓存由L1、L2和L3三个层级组成,每个层级的缓存容量逐级增大,但访问延迟也逐级增加。
当程序进行物理内存访问时,系统首先会检查L1缓存,也称为指令缓存(Instruction Cache)和数据缓存(Data Cache),来确定是否存在所需数据的副本。对Cache的访问需要把一个物理地址分为标记(CT)、组索引(CI)、块偏移(CO)三个部分。首先我们通过组索引来找到我们的地址在Cache中所对应的组号,再通过标记和Cache的有效位来判断我们的内容是否在Cache中。如果数据在L1缓存中命中,即缓存命中,系统可以直接从L1缓存中获取所需数据,从而避免了访问主内存的开销,提高了访问速度。
如果数据在L1缓存未命中,系统会继续检查L2缓存,也称为高速缓存(Unified Cache)。L2缓存容量比L1缓存大,但访问延迟也相应增加。如果数据在L2缓存中命中,系统将从L2缓存中获取数据,避免了直接访问主内存,尽管相比于L1缓存,访问速度略有下降。当数据在L2缓存未命中时,系统将进一步检查L3缓存。
如果数据在三级缓存中都未命中,系统将通过主内存来获取所需数据。此时会发生缓存不命中(Cache Miss),系统将从主内存中读取数据,并将其加载到适当的缓存层级中,以供后续访问使用。
值得注意的是:缓存的容量有限,如果数据无法在缓存中找到,就会发生缓存不命中,需要从主内存中获取数据,这会导致较高的访问延迟。因此,在设计程序时,需要合理利用缓存,并尽可能提高缓存的命中率,以获得更好的性能。
7.6 hello进程fork时的内存映射
当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
通过execve函数,Hello进程能够加载一个新的可执行文件,替换当前进程的执行内容。并且,execve函数加载新程序时会替换当前进程的内存映射,原来的代码和数据会被新程序的内容取代。
加载并运行hello需要以下结构步骤:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个相应的虚拟地址,而与该地址相应的物理页面不在内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。
7.9动态存储分配管理
动态储存分配管理使用动态内存分配器来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。动态内存分配主要有两种基本方法与策略:
(1)带边界标签的隐式空闲链表分配器管理:
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。
隐式空闲链表:在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
(2)显示空间链表管理:
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
显式空闲链表:在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
在这章,我们深入了解了Hello程序中的存储管理过程。在fork进程时,通过页表和写时复制技术实现了父子进程之间的内存映射,避免了完全复制内存的开销。而在execve时,通过重新映射程序的逻辑地址空间,将新的程序加载到内存中,并更新页表,实现了内存的重新映射。并且详细学习了虚拟地址与物理地址之间映射变换的全过程以及缺页故障和缺页中断处理的过程。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m字节的序列:B0,B1,B2……Bm
所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O 接口:
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息。
(2)Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。
(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
(4)读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文 件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢 复到可用的描述符池中去。
Unix I/O 函数:
(1)int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。 open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。
(2)int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。
(3) ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为 fd 的当前文 件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。
4) ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。
8.3 printf的实现分析
首先查看函数printf的函数体:
int printf(const char *fmt, ...)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
va_list是字符指针,而(char*)(&fmt + 4)表示fmt后的第一个参数的地址。
vsprintf函数的返回值是格式化后的字符串的长度,其功能是根据fmt中指定的格式标识符将可变参数列表转换为相应的格式。
write函数是一个系统调用,用于将buf中的i个字节写入到文件描述符handle所指向的文件中,返回值是实际写入的字节数,如果发生错误则返回-1。
write函数在执行时,会通过eax寄存器传递系统调用号_NR_write,通过ebx和ecx寄存器传递参数handle和buf,然后触发中断向量INT_VECTOR_SYS_CALL,进入内核态并调用sys_call函数。为了保护进程的状态,在进入内核态之前会保存当前进程的寄存器和栈信息。
字符显示驱动程序负责将ASCII码转换为对应的字模数据,并将其写入到显存vram中。显存vram是一个二维数组,每个元素存储一个像素点的RGB颜色值。
显示芯片按照一定的刷新频率逐行扫描显存vram,并通过信号线向液晶显示器发送每个像素点的RGB颜色值,从而在屏幕上显示出文字。
8.4 getchar的实现分析
getchar函数是一个标准库函数,用于从标准输入流(stdin)中读取一个字符,并返回其ASCII码值。它的原型是:
int getchar(void);
getchar函数的实现分析可以从以下几个方面进行:
getchar函数的内部实现通常会调用read系统调用,向内核请求从文件描述符0(即标准输入)中读取一个字节的数据,并将其存放在一个缓冲区中。read系统调用的原型是:
ssize_t read(int fd, void *buf, size_t count);
其中,fd是文件描述符,buf是缓冲区指针,count是请求读取的字节数。read系统调用返回实际读取的字节数,或者在出错时返回-1。
当程序调用getchar函数时,如果标准输入流中没有可用的数据,那么程序就会阻塞等待用户输入。用户输入的字符被键盘驱动程序接收并转换为ASCII码,然后存放在系统的键盘缓冲区中,直到用户按下回车键为止(回车字符也放在缓冲区中)。这个过程涉及到异步异常-键盘中断的处理。当键盘发生中断时,内核会调用键盘中断处理子程序,从键盘控制器中读取按键扫描码,并将其转换为ASCII码,然后保存到系统的键盘缓冲区中。
当用户按下回车键后,getchar函数才开始从标准输入流中每次读取一个字符,并返回其ASCII码值。如果标准输入流已经到达文件结尾(EOF),那么getchar函数会返回-1。同时,getchar函数还会将用户输入的字符回显到标准输出流(stdout)上,以便用户看到自己输入的内容。
如果用户在按下回车键之前输入了多个字符,那么这些字符会保留在系统的键盘缓冲区中,等待后续的getchar函数调用读取。也就是说,后续的getchar函数调用不会等待用户输入,而是直接从缓冲区中读取一个字符,并返回其ASCII码值。直到缓冲区中的字符读完为止,才会再次等待用户输入。
8.5本章小结
本章介绍了 Linux 的 I/O 设备的基本概念和管理方法,以及Unix I/O 接口及其函数。最后分析了printf 函数和 getchar 函数的工作过程。
结论
编写一个输出“Hello, world!”的hello.c源文件
使用gcc命令对hello.c进行预处理、编译、汇编和链接,生成可执行程序hello
使用./hello命令运行可执行程序hello
shell进程调用fork()函数创建一个子进程,并复制父进程的地址空间和状态
子进程调用execve()函数加载可执行程序到虚拟内存,并覆盖原来的地址空间和状态
MMU负责将程序中的虚拟地址转换为物理地址,并检查地址是否合法和可访问
处理器根据程序中的指令执行逻辑控制流,包括顺序执行、条件分支、循环等
程序正常结束或者出现异常时,调用信号处理函数进行处理,并返回相应的退出码
进程结束后,操作系统负责释放和回收其占用的资源,包括内存、文件描述符等
附件
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行文件
hello:链接之后的可执行文件
hello_o.elf:hello.o的ELF格式
hello.elf:hello的ELF格式
hello_o.objdump:hello.o反汇编代码
hello.objdump:hello的反汇编代码
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
(参考文献0分,缺失 -1分)