摘 要
本文通过一个结构简单的hello程序,说明了由文本到可执行的文件,再到一个进程的整个过程中,计算机系统都起到了怎样的作用。在这一过程中,我们会接触到程序预处理、编译、汇编、链接等过程,同时也介绍了有关进程、存储、I/O等相关的知识,深入系统底层的软硬件结合的部分,对计算机系统进行了较为系统和广泛的探讨。
第1章 概述
1.1 Hello简介
Hello.c是一个以字节序列方式储存在文件中的源程序(Program),经过预处理器(cpp)变为Hello.i,然后在编译器(ccl)的处理后变为Hello.s的汇编程序,然后通过汇编器(as),生成了Hello.o的可重定位目标程序(二进制),然后在链接器(ld)的链接后,生成可执行的Hello目标程序。在编译系统之后,调用shell命令行输入,fork产生子进程,Hello从program成为Process计算机完成了From Program to Process的过程:

1.2 环境与工具
1.2.1 硬件环境
AMD Ryzen 7 5800H 3.2GHz; 16G RAM;
1.2.2 软件环境
Windows 11 21H2; VMware Workstation 16.2.3; Ubantu 20.04
1.2.3 开发工具
Visual Studio 2022; Codeblocks 20.03
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
1.4 本章小结
本章主要介绍了Hello作为一个程序、进程经历的生命周期,解释了何为程序的P2P、020,记录了实验时的软硬件环境,以及生成的中间文件。
第2章 预处理
2.1 预处理的概念与作用
预处理是预处理器(cpp)根据#开头的命令,修改原始的C程序,将源程序及引用的库合并成完整的文件,得到了另一个C程序,一般以.i作为文件扩展名。
C语言的预处理主要有三个方面的内容:
1.宏定义 #define;
2.文件包含 #include;
3.条件编译 #if #else #elif #ifndef #ifdef等;[1]
预处理的作用是从系统的头文件包中将头文件的源码插入到目标文件中,在编译代码前首先将标识符替换好,确保程序的完整性,生成.i文件后再进行接下来的编译工作。
2.2在Ubuntu下预处理的命令
在命令行窗口中使用命令gcc -m64 -no-pie -fno-PIC -E hello.c > hello.i,生成hello.i文件,截图如下:

2.3 Hello的预处理结果解析

hello.c经过预处理后得到了hello.i文件,其文本量大大增加,达到了三千余行,其中main函数处于文档的尾部。可以发现,该文本文件已经解析了hello.c中引用的头文件,在文本的前几行,我们可以看到如下内容:

接下来还有很多对于库中预置的函数的定义:

2.4 本章小结
本章主要介绍了对.c文件的预处理,通过对于生成的.i文件的分析,我们可以了解到预处理器对于源程序做了哪些处理——例如对于宏的替换,引入各种需要的头文件等。
第3章 编译
3.1 编译的概念与作用
编译,就是编译器通过词法分析和语法分析,确认所有指令都是合法的,然后将其翻译为等价的汇编代码。
编译器会将.i文件翻译为.s文本文件,其中包含了一个汇编语言程序。汇编语言是一种通用的、接近底层的语言,即使是不同高级语言的不同编译器,最后也会得到通用的汇编语言,进而进行下一步的实现。
3.2 在Ubuntu下编译的命令

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

其中值得我们注意的是.section .rodata声明了接下来的内容是只读类型的,该声明用于维护只读数据,比如:常量字符串、带 const 修饰的全局变量和静态变量等[2],在本程序中,需要打印的字符串在.rodata中声明;而.text则是声明是程序代码段;.align 8声明了本汇编语言程序是以8个字节的倍数来进行内存对齐。
3.3.2 转移控制
在main函数中,汇编语言用.cfi_startproc和.cfi_endproc声明了函数的起始,其中涉及了条件跳转语句,例如对于C语言中的if (argc != 4),有如下汇编语句,其中%rbp中存储位置处是常量4:
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $4, -20(%rbp)
je .L2
对于C程序中for循环中的循环语句for(i=0;i<8;i++)
汇编代码为
.L3:
cmpl $7, -4(%rbp)
jle .L4
3.3.3 数据
初始化的全局变量储存在.data节,它的初始化不需要汇编语句,而是直接完成的。而局部变量存储在寄存器或栈中。在该程序中的局部变量i就用到了栈,汇编代码如下:
.L2:
movl $0, -4(%rbp)
jmp .L3
3.3.4 算术操作
程序的for循环中有自加操作符,在汇编中每次循环都通过跳转指令实现,循环体中就包含了addl $1, -4(%rbp),对栈中存储的i加1
3.3.5数组/指针/结构操作
main函数的参数中含有指针数组char *argv[],在argv数组中,argv[0]指向输入程序的路径和名称,argv[1]和argv[2]分别表示两个字符串,我们不妨以它们为例,追踪指针数组存储在程序中的哪部分。
.LFB6:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp) //argc存储在edi中
movq %rsi, -32(%rbp)//argv存储在rsi中
cmpl $4, -20(%rbp)
je .L2
movl $.LC0, %edi
call puts
movl $1, %edi
call exit
栈中%rsi-8和%rax-16的位置,分别存储着argv[1]和argv[2]两个字符串。
3.3.6 函数操作
main函数:
参数传递:传入参数argc和argv[],分别位于寄存器rdi和rsi中。
函数调用:被系统启动函数调用。
函数返回:设置%eax为0并且返回,对应return 0
exit函数:
参数传递:传入的参数为1,再执行退出命令
函数调用:if判断条件满足后被调用
函数返回:退出程序,正确退出,返回0;出现错误,返回非0值
sleep函数:
参数传递:传入参数atoi(argv[3]),
函数调用:for循环下被调用,call sleep
汇编代码如下:
.L4:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
movl $.LC1, %edi
movl $0, %eax
call printf
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rax
movq %rax, %rdi
call atoi
movl %eax, %edi
call sleep
addl $1, -4(%rbp)
3.4 本章小结
本章主要分析了hello.s汇编语言文本文件的内容,分析了编译器对于源程序的操作,深入分析了汇编文件中是如何实现C语言的数据与操作的。
第4章 汇编
4.1 汇编的概念与作用

汇编是编译后的文件到生成机器语言二进制程序的过程,机器语言指令被打包成可重定位目标程序的格式。
4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式
可以使用指令:readelf -a hello.o > hello_0.elf得到.elf的文件。
ELF头如下:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 1192 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
节头如下:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000008e 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000358
00000000000000c0 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 000000ce
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 000000ce
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 000000d0
0000000000000033 0000000000000000 A 0 0 8
[ 6] .comment PROGBITS 0000000000000000 00000103
000000000000002c 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 0000012f
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.propert NOTE 0000000000000000 00000130
0000000000000020 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 00000150
0000000000000038 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 00000418
0000000000000018 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 00000188
0000000000000198 0000000000000018 12 10 8
[12] .strtab STRTAB 0000000000000000 00000320
0000000000000032 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 00000430
0000000000000074 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
重定位的信息包括了类型和偏移量,在链接阶段会用到这些信息来进行地址的计算,需要进行重定位的信息包括了.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar等函数
重定位节如下:
重定位节 ‘.rela.text’ at offset 0x358 contains 8 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
00000000001a 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
00000000001f 000b00000004 R_X86_64_PLT32 0000000000000000 puts - 4
000000000029 000c00000004 R_X86_64_PLT32 0000000000000000 exit - 4
000000000050 00050000000a R_X86_64_32 0000000000000000 .rodata + 26
00000000005a 000d00000004 R_X86_64_PLT32 0000000000000000 printf - 4
00000000006d 000e00000004 R_X86_64_PLT32 0000000000000000 atoi - 4
000000000074 000f00000004 R_X86_64_PLT32 0000000000000000 sleep - 4
000000000083 001000000004 R_X86_64_PLT32 0000000000000000 getchar - 4

重定位节 ‘.rela.eh_frame’ at offset 0x418 contains 1 entry:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
.symtab是一个符号表,其中存放着程序中定义和引用的函数和全局变量的信息:
Symbol table ‘.symtab’ contains 17 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 9
9: 0000000000000000 0 SECTION LOCAL DEFAULT 6
10: 0000000000000000 142 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND atoi
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sleep
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND getchar
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的hello.s进行对照分析。
分析hello.o的反汇编,并与第3章的 hello.s进行对照分析:
操作数:hello.s中的操作数采用十进制来表示,而hello.o的反汇编代码中的操作数采用了十六进制。
分支转移:在hello.s的跳转语句中采用的是.L2和.LC1等段的名称,而反汇编代码中跳转指令之后是间接地址,是每条语句之间相对偏移的地址。
函数调用:hello.s中,call指令直接使用了函数名称,而反汇编代码中call指令使用的是相对于main函数的偏移地址。同时在.rela.text节中为其添加了重定位条目,待在链接之后确定物理地址。
4.5 本章小结
本章主要介绍了汇编后程序的文本文件被转化为可重定位目标程序的过程,然后通过ELF文件格式分析了我们得到的.o文件,然后通过对所得的.o文件进行反汇编,发现了经过汇编后程序的改动——用逻辑地址的相对偏移来约定跳转指令和call指令。
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被复制到内存并执行。
链接使得分离编译成为可能,我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,并对它们进行独立地修改和编译。
5.2 在Ubuntu下链接的命令

5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
在命令行中键入命令readelf -a hello > hello_1.elf得到hello的ELF格式。
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x4010f0
程序头起点: 64 (bytes into file)
Start of section headers: 14208 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 12
Size of section headers: 64 (bytes)
Number of section headers: 27
Section header string table index: 26
节头描述了各个节的大小、偏移量以及其他属性。链接时,会将各个文件的相同段进行合并,并且根据得到的段的大小以及偏移量重新设置各个符号的地址。:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 00000000004002e0 000002e0
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.propert NOTE 0000000000400300 00000300
0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.ABI-tag NOTE 0000000000400320 00000320
0000000000000020 0000000000000000 A 0 0 4
[ 4] .hash HASH 0000000000400340 00000340
0000000000000038 0000000000000004 A 6 0 8
[ 5] .gnu.hash GNU_HASH 0000000000400378 00000378
000000000000001c 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 0000000000400398 00000398
00000000000000d8 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000400470 00000470
000000000000005c 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 00000000004004cc 000004cc
0000000000000012 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 00000000004004e0 000004e0
0000000000000020 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000400500 00000500
0000000000000030 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000400530 00000530
0000000000000090 0000000000000018 AI 6 21 8
[12] .init PROGBITS 0000000000401000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000401020 00001020
0000000000000070 0000000000000010 AX 0 0 16
[14] .plt.sec PROGBITS 0000000000401090 00001090
0000000000000060 0000000000000010 AX 0 0 16
[15] .text PROGBITS 00000000004010f0 000010f0
0000000000000145 0000000000000000 AX 0 0 16
[16] .fini PROGBITS 0000000000401238 00001238
000000000000000d 0000000000000000 AX 0 0 4
[17] .rodata PROGBITS 0000000000402000 00002000
000000000000003b 0000000000000000 A 0 0 8
[18] .eh_frame PROGBITS 0000000000402040 00002040
00000000000000fc 0000000000000000 A 0 0 8
[19] .dynamic DYNAMIC 0000000000403e50 00002e50
00000000000001a0 0000000000000010 WA 7 0 8
[20] .got PROGBITS 0000000000403ff0 00002ff0
0000000000000010 0000000000000008 WA 0 0 8
[21] .got.plt PROGBITS 0000000000404000 00003000
0000000000000048 0000000000000008 WA 0 0 8
[22] .data PROGBITS 0000000000404048 00003048
0000000000000004 0000000000000000 WA 0 0 1
[23] .comment PROGBITS 0000000000000000 0000304c
000000000000002b 0000000000000001 MS 0 0 1
[24] .symtab SYMTAB 0000000000000000 00003078
00000000000004c8 0000000000000018 25 30 8
[25] .strtab STRTAB 0000000000000000 00003540
0000000000000158 0000000000000000 0 0 1
[26] .shstrtab STRTAB 0000000000000000 00003698
00000000000000e1 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
从Data Dump中可以看到程序从0x400000开始:

edb中程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000002a0 0x00000000000002a0 R 0x8
INTERP 0x00000000000002e0 0x00000000004002e0 0x00000000004002e0
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000005c0 0x00000000000005c0 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000245 0x0000000000000245 R E 0x1000
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
0x000000000000013c 0x000000000000013c R 0x1000
LOAD 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001fc 0x00000000000001fc RW 0x1000
DYNAMIC 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001a0 0x00000000000001a0 RW 0x8
NOTE 0x0000000000000300 0x0000000000400300 0x0000000000400300
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000320 0x0000000000400320 0x0000000000400320
0x0000000000000020 0x0000000000000020 R 0x4
GNU_PROPERTY 0x0000000000000300 0x0000000000400300 0x0000000000400300
0x0000000000000020 0x0000000000000020 R 0x8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001b0 0x00000000000001b0 R 0x1
其含义如下:
PHDR:程序头表
INTERP:程序执行前调用的解释器
LOAD:程序目标代码和常量信息
DYNAMIC:动态链接器所使用的信息
NOTE::辅助信息
GNU_EH_FRAME:异常信息保存
GNU_STACK:使用系统栈所需要的权限信息
GNU_RELRO:保存在重定位之后只读信息的位置
可以看到最开始是GNU_STACK,而LOAD从0x400000开始,接着是PHDR,INTERP和NOTE。最后是DYNAMIC和GNU_RELRO部分。
5.5 链接的重定位过程分析



重定位:链接器在完成符号解析以后,会将代码中的每个符号引用和对应的符号定义关联起来。此时,链接器会获得输入目标模块中的代码节和数据节的确切大小,进而进行重定位步骤。在这个步骤中,链接器会合并输入模块,并为每个符号分配运行时的地址。然后在重定位节中的符号引用中,链接器会修改hello中的代码节和数据节中的符号引用,使得他们指向正确的运行地址。
我们可以发现,通过对链接后的hello程序进行反汇编,所有指令的地址都变成了绝对地址,而并非hello.o中的相对地址。
5.6 hello的执行流程
使用edb执行hello,从加载hello到_start到call main以及程序终止的所有函数及其地址:
401000 _init>
401020 .plt>
401030 puts@plt>
401040 printf@plt>
401050 getchar@plt>
401060 atoi@plt>
401070 exit@plt>
401080 sleep@plt>
401090 _start>
4010c0 _dl_relocate_static_pie>
4010c1 main>
401150 __libc_csu_init>
4011b0 __libc_csu_fini>
4011b4 _fini>
5.7 Hello的动态链接分析
共享链接库代码是一个动态的目标模块,在程序开始运行或者调用程序加载时,可以自动加载该代码到任意的一个内存地址,并和一个在目标模块内存中的应用程序链接了起来,这个过程就是对动态链接的重定位过程。
在elf文件中,有:
.got PROGBITS 0000000000403ff0 00002ff0
.got.plt PROGBITS 0000000000404000 00003000
0x0000000000000003 (PLTGOT) 0x404000
在edb中查看:


5.8 本章小结
本章介绍了有关链接的概念和作用,分析了hello的ELF格式以及虚拟地址空间是如何进行分配的,介绍了重定位和动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。
6.2 简述壳Shell-bash的作用与处理流程
在Linux系统中,Shell是一个交互型应用级程序,为用户提供了与系统内核进行交互的方式。
主要功能是:Shell读取输入->处理输入内容,获取输入参数->如果是内核命令则直接执行,否则调用程序->当程序运行时,shell会监视用户输入并对此做出响应。
具体的处理流程如下:
1.Shell从命令行中读入特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:
SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2.处理tokens块,检查看他们是否是shell中所引用到的关键字。
3.当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第1个关键词。如果成功找到,执行替换操作并且处理过程回到第1步再次处理程序块tokens。
4.Shell对~符号进行替换,对所有前面带有符号的变量进行替换。5.将命令行中内嵌的符号的变量进行替换。 5.将命令行中内嵌的符号的变量进行替换。5.将命令行中内嵌的(command)命令表达式替换成命令
6.计算被$(expression)标记的算术表达式。
7.Shell将命令串重新划分为新的tokens块。依栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB和\n,并替换通配符,例如:* ? [ ]。
8.shell按照下面的顺序检查命令:
内置命令->用户自定义函数->按路径寻找可执行的脚本文件
9.对所有的输入输出重定向进行初始化,最终执行命令
6.3 Hello的fork进程创建过程
根据shell的处理流程,可以推断,输入命令执行hello后,父进程如果判断不是内部指令,即会通过fork函数创建子进程。子进程与父进程近似,并得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈。父进程打开的文件,子进程也可读写。二者之间最大的不同或许在于PID的不同。Fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法来分辨程序是在父进程里还是子进程里执行。
6.4 Hello的execve过程
execve函数在加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。该函数的作用就是在当前进程的上下文中加载并运行一个新的程序。
只有当出现错误时,例如找不到Hello时,execve才会返回到调用程序,这里与一次调用两次返回的fork不同。
在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:
int main(intargc ,char **argv, char *envp)
结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello:
删除已存在的用户区域(自父进程独立)。
共享区映射:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
私有区映射:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
设置PC:exceve会设置当前进程的上下文中的程序计数器,指向代码区的入口。
6.5 Hello的进程执行
逻辑控制流:
一系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。
用户模式和内核模式:
处理器使用一个寄存器提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文:
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
初始时,控制流位于hello进程内,处于用户模式
调用系统函数sleep后,进入内核态,此时间片停止。
2s后,发送中断信号,转回用户模式,继续执行指令。
调度的过程:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
以执行sleep函数为例,sleep函数请求调用休眠进程,sleep将内核抢占,进入倒计时,当倒计时结束后,hello程序重新抢占内核,继续执行。

用户态与核心态转换:
为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
hello执行过程中可能会出现以下异常:
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 返回到下一条指令
陷阱 有意的异常 同步 返回到下一条指令
故障 潜在的可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回
运行程序时,参数是学号+姓名+秒数,在命令行中键入./hello 120L021327 DAIHONGQIAN 1,可以得到如下结果:

执行ctrl+Z命令,可以将正在运行的hello程序挂起,我们键入ps命令,可以查看正在运行的进程的PID,hello进程的PID为3399,再使用jobs命令查看正在运行的后台任务,也可以看到我们刚刚键入的./hello 120L021327 DAIHONGQIAN 1,此时使用fg命令,我们可以把后台的hello调回到前台继续执行。[4]

接下来,我们键入ctrl+C的终止指令,进程收到 SIGINT 信号,我们可以结束彻底结束hello进程,这时我们再通过ps和jobs命令检查正在运行的命令,我们发现hello进程已经结束。

中途随意从键盘键入字符,都会被接收至缓冲区,命令行无法解析无意义的字符串,会提示command not found。
kill命令会杀死挂起的程序,执行kill命令后无法查询到hello的PID。

6.7本章小结
本章主要介绍了有关hello进程管理的内容,通过shell我们可以管理计算机运行的进程。我们还介绍了hello程序的fork、execve过程,然后通过命令行研究了程序运行当中可能会遇见的异常,以及我们的系统是如何处理这些由异常发出的信号的。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址,是指由程序hello产生的与段相关的偏移地址部分,hello.o文件中的地址就是逻辑地址。
线性地址,是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址——也就是段中的偏移地址,它加上相应段的基地址就会生成线性地址。
虚拟地址,是由CPU生成的一个仰赖访问主存的地址,它会被送到内存之前先转换成适当的物理地址,地址翻译的任务由CPU芯片上的内存管理单元MMU来承担,MMU会用存放在主存中的查询表来动态翻译虚拟地址,该表的内容由操作系统管理。在linux系统中,只会分页而不会分段,因此对于我们的hello程序逻辑地址几乎就是虚拟地址。
物理地址,是主存上被组织成一个由M个连续的字节大小的单元组成的数组,每个字节都有一个唯一的物理地址,hello的物理地址来自于虚拟地址的地址转换,它也是程序运行的最终地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
索引号,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
Base,它描述了一个段的开始位置的线性地址。
全局的段描述符,放在全局段描述符表(GDT)中,而局部的段描述符,处于局部段描述符表(LDT)之中。GDT在内存中的地址和大小会存放在gdtr控制寄存器中,而LDT则在ldtr寄存器中。
一个完整的逻辑地址包括段选择符+段内偏移地址,
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理,是一种内存空间存储管理的技术,分为静态页式管理和动态页式管理。页式管理将各进程的虚拟空间划分成若干个长度相等的页(page),再把内存空间按页的大小划分成片,进而建立起内存地址与页式虚拟地址一一对应关系,存储在页表之中。为了解决离散地址变换问题,它会调用相应的硬件地址变换构件。页式管理采用请求调页或预调页技术实现了内外存的统一管理与调用。
优点:它有效地解决了碎片问题。由于页式管理不需要进程的程序段和数据在内存中连续存放,从而提供了内外存统一管理的虚拟内存实现方式,使用户可以利用的碎片化的空间大大增加,也非常有利于多个进程同时执行。
缺点:需要相应的硬件支持,增加了机器成本,并增加了系统开销。例如缺页中断的产生和选择淘汰页面等都要求有相应的硬件支持;缺页中断处理时,请求调页的算法如果不够恰当,有可能产生抖动现象。碎片式的管理,也使得每个进程内总有一部分空间得不到利用,当页面比较大的时候,这一部分的损失会非常显著。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:每次CPU产生一个虚拟地址,MMU就必须查阅相应的PTE,每次都必然造成缓存不命中等一系列时间开销,为了消除这样的开销,MMU中存在一个全相联高速缓存,称为TLB。
四级页表:如果只采用两级的页表结构,当需要使用的虚拟内存很小,但仍然需要建立其一个庞大的页表,这很容易造成内存的浪费,检索巨大的页表也是非常浪费性能的事。由此,在虚拟地址到物理地址的转换过程中,又开发出了多级页表的机制:上一级的页表映射到下一级页表,直到页表映射到虚拟内存。四级页表,也就指共有四级的多级页表。
7.5 三级Cache支持下的物理内存访问
物理地址被分为CT(标记)+CL(索引)+CO(偏移量),然后到1级cache里去找对应的标记位为有效的。如果命中就直接返回想要的数据,如果不命中,就依次去L2,L3,主存判断是否命中,当命中时,将数据传给CPU同时更新各级cache的cacheline。当cache已满时,需要按一定策略,将cache中近期不太可能再次访问到的数据替换为我们所需要的物理内存访问索引。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。它创建了当前进程的mm_struct、区域结构和页表的副本,将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。
7.7 hello进程execve时的内存映射
1)在bash中的进程中执行了execve调用:execve(“hello”,NULL,NULL);
2)execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello替代了当前bash中的程序。
3)删除已存在的用户区域。
4)映射私有区域
5)映射共享区域
6)设置程序计数器(PC)
exceve做的最后一件事是设置当前进程的上下文中的程序计数器,是指指向代码区域的入口点。而下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
整体的处理流程:
1.处理器生成一个虚拟地址,并将它传送给MMU
2.MMU生成PTE地址,并从高速缓存/主存请求得到它
3.高速缓存/主存向MMU返回PTE
4.PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
5.缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
6.缺页处理程序页面调入新的页面,并更新内存中的PTE
7.缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。
7.9动态存储分配管理
基本方法与策略:通过维护虚拟内存(堆),一种是隐式空闲链表,一种是显式空闲链表。显式空闲链表法是malloc(size_t size)每次声明内存空间都保证至少分配size_t大小的内存,双字对齐,每次必须从空闲块中分配空间,在申请空间时将空闲的空间碎片合并,以尽量减少浪费。分配器一般按以下策略进行分配:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
7.10本章小结
本章主要介绍了 hello 的存储器地址空间、 intel 的段式管理、 hello 的页式管理,在指定环境下介绍了 VA 到 PA 的变换、物理内存访问,还介绍 hello 进程 fork 时的内存映射、 execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化
文件(所有的I/O设备都被模型化为文件,甚至内核也被映射为文件)
设备管理
unix io接口
这种将设备映射为系统文件的方式,允许Linux内核引出一个简单的应用接口,称为Unix I/O。
文件操作包括:打开和关闭操作;读写操作;更改当前文件位置……
8.2 简述Unix IO接口及其函数
open函数
功能描述:打开或创建文件,可以指定文件的属性及用户的权限等各种参数。
函数原型:int open(const char *pathname,int flags,int perms)
参数:pathname:被打开的文件所在路径,flags:文件打开方式,
返回值:成功:返回文件描述符;失败:返回-1
close函数
功能描述:用于关闭一个被打开的的文件
所需头文件: #include <unistd.h>
函数原型:int close(int fd) 参数:fd文件描述符
函数返回值:0成功,-1出错
read函数
功能描述: 读取文件中数据
所需头文件: #include <unistd.h>
函数原型:ssize_t read(int fd, void *buf, size_t count);
参数:fd:目标文件描述符。buf:缓冲区。count:表示调用一次read操作,应该读取的字符数量。
返回值:返回所读取的字节数;-1(出错)。
write函数
功能描述:写入数据。
所需头文件:#include <unistd.h>
函数原型:ssize_t write(int fd, void *buf, size_t count);
返回值:写入文件的字节数(成功);-1(出错)
lseek()函数
功能描述: 用于在指定的文件描述符中将将文件指针定位到相应位置。
所需头文件:#include <unistd.h>,#include <sys/types.h>
函数原型:off_t lseek(int fd, off_t offset,int whence);
参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)
返回值:成功:返回当前位移;失败:返回-1
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;

}
vsprintf函数将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最后就在显示器上实现了信息的打印。
8.4 getchar的实现分析
异步异常-键盘中断的处理:
getchar可以理解为对于键盘中断的异步异常的处理。当键盘中断处理子程序。系统会接受按键输入的信息,并将其转换为ascii码,保存到键盘输入的缓冲区。getchar等调用read系统函数,通过系统调用读取按键输入数据,当接受到回车\n时才返回。getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止,回车字符\n也会被读入缓冲区中。
当用户键入回车之后,getchar开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
8.5本章小结
本章介绍了 Linux 的 I/O 设备的基本概念和管理方法,以及Unix I/O 接口及其函数。最后分析了printf 函数和 getchar 函数的工作过程。
结论
最初接触到C 语言的我们,在计算机的屏幕前,通过键盘鼠标等I/O设备键入hello程序的代码,将它保存为了.c的文本文件,我们点击codeblocks中的编译按钮,再运行可执行的文件,hello便横空出世了。对于一个初学者而言,编写这样结构简单的程序似乎不是一件难事,然而计算机系统这一节课把hello的神秘面纱缓缓解开了,我们看到了hello的“漫漫长征路”:
1.hello.c经过预编译,拓展得到hello.i文本文件
2.hello.i经过编译,得到汇编代码hello.s汇编文件
3.hello.s经过汇编,得到二进制可重定位目标文件hello.o
4.hello.o经过链接,生成了可执行文件hello
5.bash进程调用fork函数,生成子进程;并由execve函数加载运行当前进程的上下文中加载并运行新程序hello
6.hello的变化过程中,会有各种地址,但最终我们真正期待的是PA物理地址。
7.hello再运行时会调用一些函数,比如printf函数,这些函数与linux I/O的设备模拟化密切相关
8.hello最终被shell父进程回收,内核会收回为其创建的所有信息
hello的一生并不是一条平坦的大路,它更像是个通过神秘穿梭机来回变换的时间旅客,每一步的变换都极端复杂,让人琢磨不透。然而通过计算机系统的学习,我们可以将这些“魔术”背后的奥秘一一揭晓,由0和1,人类的智慧一步步将它变为高楼大厦,一切复杂的系统背后都是非常质朴的想法,为了贴切地把这个想法落地生根,精妙而复杂的系统就被建立了起来。
我们现在所使用的计算机系统,经过了UI的改革,变得直观易懂,而繁琐的系统实现的细节被悄悄地包装在了我们看不见的深处里,这门课程之所以学习接近底层的那些细节,不厌其烦地探讨那些被藏起来的内容,能很好地帮助我们认清计算机系统的实质。也许科技发展到怎样的地步,我们提起骇客、提起极客这类名词时,脑海中浮现的都是那个手指飞快地敲击键盘,屏幕上是荧光绿色的命令行,一切可能,皆系于其中。

附件
预处理后的文件 hello.i
编译之后的汇编文件 hello.s
汇编之后的可重定位目标文件 hello.o
链接之后的可执行目标文件 Hello
Hello.o 的 ELF 格式 elf.txt
Hello.o 的反汇编代码 Disas_hello.s
hello的ELF 格式 hello1.elf
hello 的反汇编代码 hello1_objdump.s
参考文献
[1] 条件编译,C语言条件编译详解. http://c.biancheng.net/view/289.html
[2] .bss、.data 和 .rodata section 之间的区别. https://blog.csdn.net/wohenfanjian/article/details/106007978
[3] 信息安全系统设计基础. 异常控制流.
[4] linux后台运行、挂起、恢复进程相关命令https://blog.csdn.net/koberonaldo24/article/details/103136125
[5] printf 函数实现的深入剖析. https://www.cnblogs.com/pianist/p/3315801.html

HIT计算机系统大作业——hello的一生相关推荐

  1. HIT 计算机系统大作业 Hello程序人生P2P

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 人工智能(未来技术模块) 学 号 7203610725 班 级 2036014 学 生 黄鸿睿 指 导 教 师 刘宏伟 计算机科学 ...

  2. 计算机系统-大作业-hello的一生-哈尔滨工业大学2020级

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学   号 120L022417 班   级 2003008 学       生 董亦轩 指 导 教 ...

  3. 2021春深入理解计算机系统大作业---hello的一生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学    号 120L021725 班    级 2003006 学       生 杨楠 指 导 ...

  4. HIT计算机系统大作业-程序人生-Hello’s P2P

    计算机系统大作业 ** 由于采用静态部署,需要看图片详细分析的小伙伴请移步个人博客网站:** 个人博客 题目:程序人生-Hello's P2P 学号: 姓名:熊峰 摘要: hello程序作为最简单的. ...

  5. HIT计算机系统大作业2022

    计算机系统 大作业 计算机科学与技术学院 2021年5月 摘 要 学习计算机系统后,从底层对一个简单的Hello.c如何一步步编译,链接,加载,执行等进行浅层的描述. 关键词:计算机系统,底层. 目 ...

  6. 2022秋计算机系统大作业-hello的一生

    计算机系统 计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机科学与技术 计算机科学与技术学院 2022年5月 摘  要 本文通过详细分析hello从预处 ...

  7. HIT计算机系统大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学 号 120L022305 班       级 2003008 学       生 XXX 指 导 ...

  8. HIT 计算机系统 大作业 程序人生-Hello’s P2P

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机类 学 号 1180300223 班 级 1803002 计算机科学与技术学院 2019年12月 摘 要 本文主要介绍了一个 ...

  9. 哈工大计算机系统大作业 HELLO的一生

    程序人生 HELLO's P2P 计算机科学与技术 吴嘉阳 2021113679 摘  要 本文串联计算机系统所学知识,以hello.c程序为例,阐述它在linux系统x86-64环境下从编写到运行终 ...

  10. HIT计算机系统 大作业 程序人生

    摘  要 通过分析hello.c的从程序到进程的过程,展示本学期计算机系统课所学的部分内容:程序的编译.链接,进程,内存,I/O.hello.c经过预处理.编译.汇编.链接成为一个可执行程序,shel ...

最新文章

  1. python write和writelines的区别_简单了解Python write writelines区别
  2. 一、稀疏数组的实际应用和代码实现
  3. AI传教士和野人渡河问题-实验报告
  4. ASP.NET MVC Framework 系列
  5. SD-WAN能带来什么好处?
  6. 【XAudio2】8.怎么播放音效
  7. Pycharm中配置Pyflink
  8. 判断字符串出现次数最多的字符 及 次数
  9. ubuntu 如何登录远程服务器_VSCode远程登录云服务器、树莓派实现在线调试代码...
  10. 20145315 《信息安全系统设计基础》第14周学习总结
  11. wordpress-Sakurairo美化主题模板
  12. Linux下超大硬盘分构(GPT分区)
  13. Caffe 议事(一):从零开始搭建 ResNet 之 残差网络结构介绍和数据准备
  14. [ios开发]锁屏后的相机的方向检查,与图片的自动旋转
  15. 搜索引擎网页排序算法
  16. 邮件误删不用怕,试试这个方法帮你找回来
  17. ARM汇编初探---汇编代码中都有哪几类指令---ARM伪指令介绍
  18. Robot Framework自动化测试教程-通过RIDE创建工程、测试套、测试用例、测试资源、变量文件,引入测试库
  19. Android开源库V - Layout:淘宝、天猫都在用的UI框架,赶紧用起来吧!
  20. 【算法】二分法多种情况详解

热门文章

  1. php防止sql注入的方法
  2. 服务器系统小米随身wifi,win8.1系统安装小米随身wifi驱动详细操作步骤【图文教程】...
  3. 图解Java 垃圾回收机制
  4. 精读-软件测试的艺术之模块测试及更高级别的测试
  5. 迅雷U享版 v3.0.1.96 Lite V4 精简绿色版
  6. 利用loic工具进行doss教程(附下载链接官方无后门)
  7. NFC reader ( ISO 15693 ) NFC相关技术详解(附源代码)
  8. SWAT模型学习(二)
  9. .Net .Net Core 下使用FastDFS 文件上传下载
  10. Windows系统字体和系统应用字体