哈工大计算机系统期末大作业-程序人生
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能2+x
学 号 2022110535
班 级 22WL029
学 生 韩晓峰
指 导 教 师 郑贵滨
计算机科学与技术学院
2024年5月
摘 要
本论文旨在深入解析“Hello”程序在计算机系统中的生命周期,从源代码到最终执行的全过程。通过构建一个P2P网络模型,我们模拟了信息在分布式系统中的传播过程。研究的核心内容包括预处理、编译、汇编、链接、进程管理、存储管理和I/O管理等关键环节。
在预处理阶段,我们探究了预处理器怎样处理源代码中的宏界说和文件包罗指令。编译阶段,我们分析了编译器怎样将预处理后的文件转换成汇编语言程序,并进一步探究了编译器对C语言数据范例和操纵的处理机制。汇编阶段,我们研究了怎样将汇编语言程序转换成呆板语言二进制程序,并分析了ELF格式的可重定位目标文件。
链接阶段,我们详细分析了静态链接过程,探究了链接器怎样将多个目标文件以及库文件链接成最终的可执行文件。在进程管理章节,我们讨论了进程的生命周期,包括创建、执行和终止,并分析了信号处理和异常处理机制。存储管理部分,我们深入探究了虚拟内存、页式管理和段式管理等概念,并分析了动态存储分配和内存映射。
I/O管理章节中,我们分析了Linux系统中的设备管理方法和Unix I/O接口,以及标准I/O函数如printf和getchar的实现原理。
末了,我们总结了“Hello”程序所经历的各个阶段,并基于此提出了对计算机系统计划与实现的一些创新理念和方法。本研究不光加深了对计算机系统工作原理的理解,而且为分布式系统的信息传播提供了新的视角。
关键词:程序生命周期;计算机系统;编译;汇编;链接:P2P网络;存储管理;I/O管理
(择要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
1.2 环境与工具
1.3 中心结果
1.4 本章小结
第2章 预处理
2.1 预处理的概念与作用
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
2.4 本章小结
第3章 编译
3.1 编译的概念与作用
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.4 本章小结
第4章 汇编
4.1 汇编的概念与作用
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.4 Hello.o的结果解析
4.5 本章小结
第5章 链接
5.1 链接的概念与作用
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
5.4 hello的虚拟地点空间
5.5 链接的重定位过程分析
5.6 hello的执行流程
5.7 Hello的动态链接分析
5.8 本章小结
第6章 HELLO进程管理
6.1 进程的概念与作用
6.2 简述壳Shell-bash的作用与处理流程
6.3 Hello的fork进程创建过程
6.4 Hello的execve过程
6.5 Hello的进程执行
6.6 hello的异常与信号处理
6.7本章小结
第7章 HELLO的存储管理
7.1 hello的存储器地点空间
7.2 Intel逻辑地点到线性地点的变更-段式管理
7.3 Hello的线性地点到物理地点的变更-页式管理
7.4 TLB与四级页表支持下的VA到PA的变更
7.5 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
7.7 hello进程execve时的内存映射
7.8 缺页故障与缺页中断处理
7.9动态存储分配管理
7.10本章小结
第8章 HELLO的IO管理
8.1 Linux的IO设备管理方法
8.2 简述Unix IO接口及其函数
8.3 printf的实现分析
8.4 getchar的实现分析
8.5本章小结
结论
附件
参考文献
第1章 概述
1.1 Hello简介
"Hello"程序的生命周期通过P2P和020两个阶段来描述,这两个阶段共同描绘了程序从编写到执行,再到资源采取的完整过程。
P2P过程(Program to Process):
在P2P阶段,"Hello"程序从源代码开始其旅程。程序员起首编写`hello.c`文件,这是一个文本文件,包罗了程序的C语言源代码。随后,预处理器介入,处理源代码中的宏界说和文件包罗指令,生成一h个中心文件`hello.i`。接着,编译器将`hello.i`转换成汇编语言,创建`hello.s`文件。汇编器随后将汇编语言翻译成呆板可以或许理解的二进制代码,生成`hello.o`这个可重定位目标文件。最终,链接器将`hello.o`与系统库中的代码链接起来,创建出一个可执行文件。
在用户通过Shell输入`./hello`命令执行程序时,Shell起首调用`fork`系统调用创建一个新的进程。这个新进程随后通过`execve`系统调用加载并开始执行`hello`程序。此时,"Hello"程序已经从一个文本文件转变为一个活泼的进程,准备在计算机上运行。
020过程(From Zero to Zero):
020过程描述了"Hello"程序在内存中的生命周期,即从无到有,再回到无的状态。一开始,程序并不占用任何内存资源。当Shell通过`fork`和`execve`启动程序时,操纵系统为新进程分配须要的内存空间,程序的代码和数据被加载到内存中,"Hello"程序开始占据资源。
随着程序的执行,CPU分配时间片给`hello`进程,通过TLB(Translation Lookaside Buffer)和分页机制,确保程序所需的数据可以或许从磁盘加载到寄存器,并最终在显示器上输出结果。当程序运行结束,操纵系统采取分配给该进程的所有资源,包括内存空间和打开的文件等。进程的信息从系统中被清除,"Hello"程序再次回到无状态,完成了它的生命周期。
通过P2P和020过程,我们可以看到"Hello"程序怎样在计算机系统中经历从源代码到进程的转变,以及它怎样在执行完毕后开释所有资源,恢复到最初的状态。这个过程不光展示了程序的执行机制,也表现了计算机系统资源管理的高效性和灵活性。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开辟与调试工具。
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk
软件环境:Windows11 64位;VMware Workstation Pro 17;Ubuntu 20.04.4
开辟和调试工具:gcc,as,ld,vim,edb,gdb,readelf,CodeBlocks
1.3 中心结果
列出你为编写本论文,生成的中心结果文件的名字,文件的作用等。
hello.c:源文件
hello.i:预处理后生成的文件
hello.s:编译后生成的文件
hello.o:汇编后生成的文件,为可重新定位目标的程序
hello:可执行目标文件
hello.asm:反汇编生成的文件
hello.elf:elf格式文件
1.4 本章小结
简朴介绍了hello.c文件,并简述了其P2P和020过程,列出来软硬件环境和开辟与调试工具,举出了hello生成过程中每一步的中心结果。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理是编译源代码成为可执行程序的第一步,它涉及对原始文本文件举行一系列的处理,以准备举行后续的编译工作。预处理的主要作用可以流畅地描述如下:
预处理器起首辨认并执行宏界说的替换。宏是一种预处理指令,允许程序员界说一段代码,这段代码在编译时会被替换成其对应的睁开情势。这为编写可重用的代码片段提供了便利,同时也使得代码更加简洁。
接着,预处理器处理文件包罗指令。在C语言中,#include指令告诉预处理器将另一个文件的内容包罗到当前文件中。这通常用于包罗库文件的声明,使得程序员不必重复编写相同的代码。
预处理器还负责条件编译的处理。条件编译允许根据不同的编译条件包罗或清除代码块。这在创建可移植的软件或根据不同平台编译相同代码时非常有效。
别的,预处理器还举行错误检查,比如检查宏是否被正确界说,以及是否正确地包罗了须要的头文件。这有助于在编译过程的早期阶段就捕捉到潜在的题目。
总的来说,预处理是编译过程中的一个关键阶段,它为编译器提供了须要的信息,确保了代码的模块化和可重用性,同时也进步了代码的可读性和维护性。通过预处理,程序员可以编写更加灵活和高效的代码。
2.2在Ubuntu下预处理的命令
应截图,展示预处理过程!
在linux下用命令gcc -E hello.c -o hello.i生成hello.i文件
图2.2.1
2.3 Hello的预处理结果解析
看图,hello.c只有24行,而hello.o有3062行。hello.i中所有注释均被删除,而且头文件也插入到了hello.i里
图2.3.1 hello.c
图2.3.2 hello.i(头文件)
图2.3.3 hello.i(末尾)
2.4 本章小结
介绍了预处理的概念和作用,而且展示了预处理生成的结果hello.i
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是将高级语言程序转换为计算机可执行的呆板语言过程,它搭建了人机交换的桥梁,不光进步了程序执行效率,还在编译阶段检查错误、增强了代码的安全性,并通过不同方式实现跨平台运行,是确保软件质量和性能的关键技术。
3.2 在Ubuntu下编译的命令
使用命令gcc -S hello.i -o hello.s,生成文件hello.s
图3.2.1 编译命令
3.3 Hello的编译结果解析
结果如下:
.file "hello.c"
.text
.section .rodata
.align 8
.LC0:
.string "\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \346\211\213\346\234\272\345\217\267 \347\247\222\346\225\260\357\274\201"
.LC1:
.string "Hello %s %s %s\n"
.text
.globl main
.type main, @function
main:
.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)
movq %rsi, -32(%rbp)
cmpl $5, -20(%rbp)
je .L2
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
.L2:
movl $0, -4(%rbp)
jmp .L3
.L4:
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rcx
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
movq -32(%rbp), %rax
addq $32, %rax
movq (%rax), %rax
movq %rax, %rdi
call atoi@PLT
movl %eax, %edi
call sleep@PLT
addl $1, -4(%rbp)
.L3:
cmpl $9, -4(%rbp)
jle .L4
call getchar@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE6:
.size main, .-main
.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
3.3.1常量
1 字符串常量
.LC0: 界说了一个字符串常量,内容是经过编码的中笔墨符,解码后可能是“你好:Hello World 程序执行完毕”。
.LC1: 界说了一个用于printf函数的格式化字符串常量,包罗三个%s占位符和一个换行符。
2 数值型常量
$5: 一个数值常量,用于比较argc的值。
$0: 一个数值常量,用于初始化循环计数器和printf的返回值。
$1: 一个数值常量,用于调用exit函数时传递退出状态。
3.3.2变量
[*]寄存器变量:
%rdi, %rsi, %rdx, %rax, %rcx: 这些寄存器用于传递函数参数和返回值,以及临时存储操纵数。
[*]栈上局部变量:
-20(%rbp): 存储main函数的参数个数argc。
-32(%rbp): 存储main函数的参数指针argv。
-4(%rbp): 用作循环计数器。
3.3.3赋值操纵
movl %edi, -20(%rbp): 将edi寄存器的值赋给栈上的局部变量,存储argc。
movq %rsi, -32(%rbp): 将rsi寄存器的值赋给栈上的局部变量,存储argv。
movl $0, -4(%rbp): 将数值0赋给栈上的局部变量,用作循环计数器的初始化。
3.3.4 范例转换
代码中没有直接的范例转换操纵,但atoi@PLT函数会将字符串转换为整数。
3.3.5算数操纵
addq $24, %rax: 将24加到rax寄存器的值上,用于计算数组元素的地点。
addq $16, %rax: 雷同地,用于计算argv中下一个参数的地点。
addq $8, %rax: 用于计算argv中当前参数的地点。
addq $32, %rax: 用于计算argv中下一个整数参数的地点。
addl $1, -4(%rbp): 将1加到循环计数器上,实现循环自增。
3.3.6关系操纵
cmpl $5, -20(%rbp): 比较-20(%rbp)(即argc)和5。
cmpl $9, -4(%rbp): 比较循环计数器和9。
3.3.7数组操纵
通过addq操纵访问argv数组的元素。
3.3.8控制转移
je .L2: 假如argc等于5,则跳转到.L2。
jmp .L3: 无条件跳转到.L3。
jle .L4: 假如循环计数器小于或等于9,则跳转到.L4。
3.3.9函数操纵
call puts@PLT: 调用puts函数。
call exit@PLT: 调用exit函数。
call printf@PLT: 调用printf函数。
call atoi@PLT: 调用atoi函数。
call sleep@PLT: 调用sleep函数。
call getchar@PLT: 调用getchar函数。
3.3.10其他
endbr64: 用于防止循环缓冲区溢出的指令。
pushq %rbp: 将基指针寄存器rbp的值推入栈中。
movq %rsp, %rbp: 设置栈帧基指针。
subq $32, %rsp: 为局部变量分配栈空间。
leave: 清理栈帧并恢复基指针。
ret: 返回到调用函数。
3.4 本章小结
本章介绍了编译的概念与作用,并分析了编译的结果。而这段汇编代码是C语言程序编译后生成的,它表现了程序执行的底层细节。代码中界说了字符串常量,用于程序的输出信息。局部变量通过栈帧机制存储,包括argc、argv和循环计数器。程序中包罗基本的赋值操纵,如将函数参数和循环计数器的值存储到栈上。算术操纵用于计算数组元素的地点和循环计数器的更新。关系操纵用于条件判断,控制程序流程。数组操纵表现在对argv数组元素的访问上。控制转移通过跳转指令实现,如循环和条件分支。函数操纵包括对标准库函数的调用,如puts、printf、atoi、sleep和getchar。别的,代码还包罗了编译器和版本信息,以及用于异常处理和栈回溯的调用帧信息指令。
整体而言,这段汇编代码是C语言程序的直接映射,它展示了程序怎样在底层硬件上执行,包括怎样处理输入参数、怎样举行条件判断、怎样执行循环、以及怎样调用函数等。通过这些汇编指令,程序可以或许完成预定的功能,如输出信息、处理用户输入和执行延时操纵。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编语言是计算机编程中的一种基础语言,它代表了呆板语言指令的文本情势,允许程序员以靠近硬件的方式编写程序。汇编语言的每条指令通常对应于CPU的一个操纵,如数据传输、算术运算、逻辑运算等。
在汇编语言中,程序员使用助记符来表现指令,这些助记符比呆板码更易于理解和影象。比方,MOV可能表现将数据从一个地方移动到另一个地方,ADD表现加法操纵。汇编语言还允许程序员直接操纵寄存器和内存地点。
由于汇编语言与具体的硬件精密相干,因此编写的程序通常具有很高的效率,但这也意味着汇编程序的可移植性较差,由于不同架构的CPU需要不同的汇编语言指令集。
汇编语言通常用于性能关键型应用、硬件驱动程序开辟、嵌入式系统编程,以及教育目标,资助学习者理解计算机系统的工作原理。然而,由于其复杂性,现代软件开辟中更常见的是使用高级编程语言,这些语言提供了更多的抽象,使得编程更加轻易和高效。高级语言编写的代码最终也会被编译器或解释器转换成汇编语言,然后进一步转换成呆板码以供执行。
作用:汇编语言在计算机系统中发挥着至关告急的作用,它使得程序员可以或许直接与硬件交互,执行底层操纵。这种能力在需要准确控制和优化性能的场景中尤为告急。比方,在开辟操纵系统、驱动程序大概嵌入式系统时,汇编语言可以或许确保代码的效率和相应速度。别的,汇编语言在教育范畴也非常告急,它资助学生理解计算机的工作原理,从最基础的层面学习程序是怎样执行的。
汇编语言还常用于性能关键型应用,由于直接编写的汇编代码可以针对特定硬件举行优化,达到高级语言难以比拟的执行速度。同时,汇编语言也是逆向工程和安全研究的告急工具,资助专业人员分析和理解已有的二进制程序,举行毛病挖掘或软件掩护步伐的破解。
总之,汇编语言虽然在现代软件开辟中不如高级语言那样常见,但它在特定范畴内的作用不可替换,是连接高级语言和硬件之间的桥梁,对于需要精细控制和高性能的场所尤其告急
4.2 在Ubuntu下汇编的命令
命令gcc hello.s -c -o hello.o生成hello.o
图4.2.1 汇编命令
4.3 可重定位目标elf格式
图4.3.1 指令生成elf格式文件
elf头:
图4.3.2 elf头
ELF头是用于界说程序文件布局的关键部分,它以一个16字节的序列开头,这个序列不光标识了文件生成系统的字大小,还指明白字节次序,比方小端序。紧接着这个序列,ELF头进一步包罗了对链接器至关告急的元数据,这些信息有助于链接器分析和理解目标文件。
在这些元数据中,包括了目标文件的范例,它可能指示文件是可执行的、可重定位的或是共享库。同时,ELF头还指定了其自身的大小,比方在某些环境下是64字节。别的,节头部表的相干信息也被包罗在内,这涉及到节头部表在文件中的偏移量,如1240字节,以及表中每个条目标大小和数量。这些信息共同支持链接器正确地处理和链接目标文件,确保程序可以或许按照预期的方式编译和运行。
节头部表:包罗名称、范例、地点、偏移等信息
图4.3.3 节头部表
节符号表:它存储了程序中界说和引用的所有符号的信息。这些符号可以是变量、函数、常量等。符号表的主要作用是在链接过程中解析程序中的符号引用,确保每个符号引用都能正确地关联到其界说。
图4.3.4 节符号表
重定位节:包罗一些hello.c中没有的函数指令
图4.3.5 重定位节
4.4 Hello.o的结果解析
图4.4.1 hello.o文件
对比hello.o的反汇编和hello.s:
两个文件hello.o和hello.s都是从hello.c源文件生成的,但它们代表了编译过程中的不同阶段。下面是对两个文件的具体比较:
文件范例:
hello.o是一个ELF二进制格式的目标文件(object file),它是经过预处理、编译、汇编后生成的,包罗了可重定位的呆板代码。
hello.s是一个文本格式的汇编源文件,它包罗了C代码对应的汇编指令,但尚未经过汇编器转换成呆板代码。
内容:
hello.o包罗了实际的呆板指令,如push %rbp,mov %rsp, %rbp等,以及一些汇编语言中的伪操纵,如endbr64,这些指令是CPU可以直接执行的。
hello.s包罗了汇编指令和数据界说,如.string用于界说字符串常量,以及伪操纵码.file,.globl,.type等,这些不是呆板指令,而是汇编器的指令和元数据。
重定位信息:
hello.o中的重定位信息(如R_X86_64_PC32和R_X86_64_PLT32)指示链接器怎样处理程序中的符号引用,这是链接过程中须要的。
hello.s中没有重定位信息,由于它是汇编代码的文本表现,还需要通过汇编器生成最终的呆板代码和重定位信息。
符号和常量:
hello.o中的符号,如puts和exit,以PLT(Procedure Linkage Table)的情势存在,这是为了支持动态链接。
hello.s中界说了字符串常量.LC0和.LC1,这些在hello.o中可能已经被转换为内存中的地点或偏移量。
可执行性:
hello.o文件本身不可执行,它需要链接器将多个目标文件合并,并解决所有外部引用,生成最终的可执行文件。
hello.s是一个源文件,需要通过汇编器转换成目标文件,然后才气参与到链接过程中。
调试信息:
hello.o可能包罗调试信息,如行号和文件名信息,这对于调试最终的可执行文件是有效的。
hello.s作为源代码的直接翻译,也可能包罗一些调试信息,但通常是以汇编语言的情势存在。
总结来说,hello.o是编译和汇编过程的最终产物,它包罗了可重定位的呆板代码和须要的元数据,准备举行链接;而hello.s是编译过程的中心产物,它以文本情势展示了C代码对应的汇编指令,需要进一步的汇编过程才气生成目标文件。
4.5 本章小结
本章介绍了汇编的概念和作用,并对可重定位目标文件的elf格式和objdump出的hello.o文件举行相识析。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接是程序编译过程中的一个关键步骤,它发生在编译器将源代码转换成汇编代码,而且这些汇编代码被汇编成呆板代码之后。链接的目标是将一个或多个目标文件(object files),这些文件包罗了编译后生成的呆板代码,以及库文件(libraries),这些文件包罗了预编译的代码,合并成一个单一的可执行文件大概库文件。
作用:
链接在软件构建过程中起着至关告急的作用,它负责将编译生成的各个目标文件以及所需的库文件合并为一个单一的可执行文件或库文件。这个过程涉及到多个关键任务:
起首,链接器通过解析程序中的符号引用,确保每个变量和函数调用都能正确地关联到其界说。这包括处理不同文件中的全局符号,解决任何潜在的命名辩论。
其次,链接器负责分配内存地点,它为程序中的代码、数据、常量平分配具体的内存空间,确定它们在可执行文件中简直切位置。
别的,链接器整合库代码,无论是静态库还是动态库,链接器都会根据程序的依靠关系将相应的库代码整合进来。静态库的代码会被直接复制到最终文件中,而动态库则会在程序运行时由操纵系统动态加载。
链接器还生成最终的可执行文件,这个文件包罗了程序的所有必须组件,可以直接被操纵系统加载和执行。
在整个链接过程中,链接器还可能举行一些优化,比如删除未使用的代码和数据,以减小最终文件的大小,进步程序的加载和执行效率。
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.2.1 链接指令
5.3 可执行目标文件hello的格式
Elf头:
图5.3.1 elf头
程序头:.o中没有
图5.3.2 程序头
节头:由13个变为27个
图5.3.3 节头
Dynamic section:.o中没有,可执行文件中有
图5.3.4 Dynamic section
符号表:不光只链接一个hello.c,以是符号表更长
图5.3.5 符号表
hello的ELF格式:
ELF头
程序头表
.init
.text
.rodata
.data
.bss
.symtab
.debug
.line
.strtab
节头部表
5.4 hello的虚拟地点空间
使用EDB(Enhanced Debugger)工具加载名为hello的程序时,我们可以查看当进步程的虚拟地点空间布局。在这个例子中,虚拟地点空间的某个部分从0x0000000000401000开始,不绝延伸到0x0000000000402000结束。通过查阅ELF(Executable and Linkable Format)程序的程序头表,我们可以确定.init等程序段的起始地点。利用这些信息,我们可以或许在EDB中准确地定位到这些段。
在EDB中,我们可以通过查看ELF表中的信息,辨认出程序中各个段的起始地点,比方.init段。这种方法允许我们映射出程序的内存布局,并在调试过程中跟踪和分析程序的行为。
图5.4.1 hello的虚拟地点空间
图5.4.2 hello文件中各字节所在地点
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
objdump得到可执行程序hello的反汇编代码。
图5.5.1 hello的反汇编代码
在比较可执行文件hello和可重定位文件hello.o的反汇编代码时,我们可以观察到几个关键的差别,这些差别揭示了链接过程的一些方面:
1、程序入口点:
在hello的反汇编代码中,程序的入口点是_start,这是Linux系统的标准程序入口点,它负责设置程序的初始执行环境,然后调用main函数。
在hello.o的反汇编代码中,我们直接看到main函数的界说,由于hello.o是一个目标文件,它尚未经过链接过程,以是它包罗了main函数的直接调用。
2、链接器生成的代码:
hello中的.init和.fini节是链接器生成的,它们包罗了程序开始和结束时执行的代码。_init通常用于初始化程序,而.fini用于清理。
在hello.o中,我们没有看到.init和.fini节,由于这些是链接器在链接过程中生成的,而不是编译器生成的。
3、程序头表和节头部表:
hello作为可执行文件,其ELF头包罗了程序头表,它界说了程序的加载方式和内存布局。
hello.o作为目标文件,包罗了节头部表,它描述了文件中的各个节(如文本、数据、符号表等),但不包罗程序头表。
4、符号解析:
在hello中,所有的符号引用都应已经解析,由于链接器会将符号引用替换为它们的最终地点。
在hello.o中,我们可以看到一些符号引用(如puts、printf、atoi、sleep和getchar),这些是外部符号,它们在链接过程中会被解析。
5、重定位信息:
hello中的重定位信息已经被处理,由于链接器已经将所有的重定位需求解决,将代码和数据放置在正确的内存地点。
hello.o包罗了重定位信息,如R_X86_64_PC32,这些信息指示链接器在最终链接时怎样处理这些引用。
6、代码和数据的布局:
在hello中,代码和数据的布局是连续的,而且已经被优化以进步执行效率。
hello.o中的布局则是按照编译器生成的次序,没有经过链接器的优化。
链接过程是将多个目标文件(如hello.o)和库文件合并成一个可执行文件(如hello)的过程。在这个过程中,链接器执行以下任务:
1、解析外部符号引用,将它们替换为最终的内存地点。
2、生成程序的程序头表和节头部表。
3、执行须要的重定位,确保代码和数据在内存中的布局正确。
4、可能还会举行代码优化和符号表的生成。
通过这些步骤,链接器确保了最终的可执行文件可以直接被操纵系统加载和执行。
5.6 hello的执行流程
使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地点。
图5.6.1 edb加载hello
hello调用与跳转的各个子程序名及程序地点如下表所示:
子程序名
hello!_start
libc-2.31.so!__libc_start_main
hello!_init
hello!main
hello!printf@plt
hello!atoi@plt
hello!sleep@plt
hello!getchar@plt
hello!exit@plt
hello!__libc_csu_init
hello!_fini
5.7 Hello的动态链接分析
查找.got的地点
图5.7.1 .got的地点
图5.7.2 init前
图5.7.3 init后
在程序初始化之后,.got表会经历告急的变化。这个表本来包罗了动态链接库中函数的初始引用地点,但在程序启动并经过动态链接器的处理之后,.got中的每个条目会被更新为指向实际目标函数的绝对内存地点。这一更新过程是动态链接器的职责,它确保了程序可以或许正确地调用已链接的动态库中的函数。
5.8 本章小结
本章介绍了链接的概念及其作用,分析了hello的elf格式虚拟地点空间,比较hello.o和hello的反汇编文件,并举行了hello的动态链接分析。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是操纵系统中的一个核心概念,它表现一个正在执行的程序的实例。当程序被加载到内存并开始运行时,它就成为一个进程。每个进程都有本身独立的内存空间,保证了进程间的相互隔离,一个进程的失败不会影响到其他进程。
进程具有动态性,它们可以被创建、执行、挂起、恢复和终止。操纵系统负责管理这些进程的生命周期。由于计算机系统中可能同时运行多个进程,操纵系统还负责调理这些进程,确保它们可以或许共享处理器资源并发执行。
进程需要使用系统资源,包括CPU时间、内存、I/O设备等,来举行它们的任务。操纵系统通过进程映像来管理这些资源,进程映像包括程序的代码、运行状态、数据和堆栈等。
进程在其生命周期中会经历不同的状态,如停当状态(等待CPU时间)、运行状态、壅闭状态(等待某些事件发生)以及终止状态。操纵系统通过分配不同的状态来控制进程的执行。
每个进程都有一个唯一的标识符,称为进程标识符(PID),它允许操纵系统和用户跟踪和管理进程。别的,进程可以通过进程间通信(IPC)机制与其他进程交换信息,这些机制包括管道、消息队列、共享内存等。
在一些系统中,进程可以创建子进程,子进程可以继承父进程的资源,也可以有本身独立的资源。这种父子关系允许复杂的程序通过分阶段的方式来执行任务。
作用:进程是操纵系统中实现多任务和资源管理的基本单位。它允很多个程序同时运行,通过操纵系统的调理,这些程序好像在并行执行,进步了系统效率和资源利用率。进程的独立内存空间确保了程序执行的安全性,防止了一个程序的异常影响其他程序。别的,进程通过系统调用或特定的IPC机制与其他进程通信和数据交换,使得复杂的任务可以分解为多个协同工作的进程。
进程的存在使得操纵系统可以或许有效地管理和调理计算机资源,同时也为用户和程序提供了一个抽象的概念,简化了程序的编写和维护。进程的生命周期管理,如创建、调理、同步、通信和终止,都是操纵系统内核的告急功能。通过这些功能,操纵系统确保了计算机系统的稳定性和高效运行。
6.2 简述壳Shell-bash的作用与处理流程
Shell,特别是Bash,作为用户与操纵系统交互的接口,起着至关告急的作用。它允许用户执行命令、管理文件系统、控制进程、配置环境变量,并通过脚本实现自动化任务。Bash通过提供一个高度可配置的环境,使用户可以或许高效地与系统沟通,执行复杂的操纵,同时简化日常任务。它还支持高级功能,如管道操纵、输入输出重定向、命令历史记载以及别名设置,这些功能共同提升了用户体验和工作效率。简而言之,Bash Shell作为一个强大的工具,它不光增强了用户对操纵系统的控制能力,也使得自动化和系统管理变得更加轻易。
处理流程:
1、读取命令:Bash 从键盘或脚本文件中读取命令。
2、解析命令:Bash 解析命令行,辨认命令和参数。
3、执行命令:Bash 根据解析结果,执行相应的程序或命令。
4、处理重定向:假如命令中包罗重定向操纵,Bash 会相应地调解输入输出流。
5、管道传递:假如命令之间使用了管道,Bash 会创建进程间通信的管道,并将一个命令的输出连接到另一个命令的输入。
6、等待命令完成:Bash 等待命令执行完成,并获取命令的退出状态。
7、反馈结果:Bash 将命令的执行结果反馈给用户,包括任何输出或错误信息。
8、记载历史:Bash 记载用户的命令历史,供未来查询和执行。
循环和条件:对于包罗循环或条件语句的脚本,Bash 会根据脚本的逻辑控制命令的执行流程。
6.3 Hello的fork进程创建过程
当一个进程在Linux系统中调用`fork()`函数时,它触发了创建一个新进程的操纵。这个新进程,也就是子进程,开始时几乎完满是父进程的一个副本,拥有本身的独立地点空间,但这个地点空间包罗了父进程的数据副本。
在`fork()`被调用的瞬间,内核执行了须要的操纵来分配新的内存空间,并复制父进程的代码段、数据段、堆和栈等。然而,子进程的栈是独立的,这样保证了父进程和子进程的独立性,由于它们可以安全地执行不同的命令而不会相互干扰。
`fork()`调用完成后,父进程会得到子进程的PID,而子进程则会收到一个0值,这是通过`fork()`函数的返回值来区分的。这个返回值允许父进程辨认和跟踪子进程的状态。
子进程继续执行`fork()`之后的代码,而父进程则可以继续它的工作大概执行其他任务。由于子进程具有本身的执行栈,它以致可以在`fork()`调用点之后执行与父进程完全不同的代码路径。
整个`fork()`过程是操纵系统内核管理进程创建和资源分配的一部分,它使得进程可以或许生成新的执行流,这对于多任务处理和并发执行至关告急。
6.4 Hello的execve过程
execve 系统调用是在类Unix操纵系统中用于执行一个新程序的过程。它替换当进步程的映像为一个新的程序。以下是execve的执行过程:
1、调用execve:进程调用execve()函数,传入要执行的程序文件名、命令行参数数组以及环境变量数组。
2、参数解析:execve起首解析传入的参数,包括程序名、参数列表和环境变量。
3、加载程序:然后,系统查找并加载指定的程序文件。这涉及到读取程序文件的头部,确定它的范例(如是否为可执行文件、脚本等),并准备执行。
4、资源分配:系统为新程序分配须要的资源,包括内存空间、打开文件描述符等。
5、地点空间设置:新程序的代码和数据被加载到进程的地点空间中,设置程序的代码段、数据段、堆和栈。
6、程序执行:一旦新程序加载并准备好执行,execve将控制权转移给新程序。新程序从它的入口点开始执行。
7、替换映像:当进步程的映像被新程序替换,进程的执行流完全改变为新程序的执行流。
8、返回值:在乐成执行时,execve通常不会返回。假如由于某些原因失败(如文件不存在或权限题目),execve会在调用进程中返回-1并设置errno以指示错误。
9、父子关系:新程序继承了原进程的某些属性,如文件描述符、环境变量等,但也可能会关闭或重置一些属性。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,叙述进程调理的过程,用户态与核心态转换等等。
1、用户输入命令:用户在Shell-bash提示符下输入./hello命令。Shell-bash解析这个命令,并准备执行它。
2、Shell调用fork:Shell通过fork系统调用创建一个新的子进程。fork复制当进步程(Shell)的地点空间到子进程,包括代码、数据、堆栈等。
3、子进程创建:fork调用后,子进程开始独立运行。在子进程中,fork返回0,而在父进程(Shell)中,fork返回子进程的PID。
4、用户态执行:子进程(即将执行hello程序的进程)处于用户态,这意味着它在执行用户空间的程序代码。
5、调用execve:子进程调用execve,加载hello程序的代码和数据到本身的地点空间,替换当前的执行映像。
6、核心态加载:execve通常涉及到切换到核心态,操纵系统内核加载程序,并设置好程序的执行环境,包括堆栈、程序计数器等。
7、进程时间片:操纵系统的调理器根据调理算法(如轮转、优先级调理等)分配时间片给hello进程。hello进程开始执行,使用分配给它的CPU时间片。
8、执行程序:hello程序开始执行,打印"Hello, World!"到标准输出。此时,进程在用户态运行,由于它在执行用户空间的程序指令。
9、系统调用:假如hello程序需要执行如打印输出等操纵,它会通过系统调用请求操纵系统的服务。这时,进程从用户态切换到核心态。
10、核心态与用户态转换:当hello程序执行系统调用时,CPU的模式从用户态切换到核心态。操纵系统内核处理完请求后,将控制权交回给用户态的进程。
11、进程结束:hello程序执行完毕后,通过exit系统调用结束进程。操纵系统内核采取资源,并将进程状态标志为终止。
12、调理其他进程:操纵系统调理器选择另一个进程运行,可能是等待的Shell或其他进程,CPU时间片被重新分配。
13、返回Shell:父进程(Shell)在fork后通过wait系统调用等待子进程(hello程序)结束。子进程结束后,Shell吸取到信号,并继续运行,等待用户输入新的命令。
整个过程中,进程调理确保了多个进程可以或许高效地共享CPU资源。用户态与核心态的转换确保了系统的安全性和进程的隔离性。Shell-bash、fork、execve等机制共同协作,为用户提供了一种简朴而强大的方式,来启动和管理进程。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不绝乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
hello执行过程中会出现的异常:中断、陷阱、故障、终止(不可恢复的致命错误造成)。
在"Hello"程序的执行过程中,可能会产生几种信号,这些信号是操纵系统用来关照进程发生了某些事件的机制。以下是一些常见的信号以及它们可能在"Hello"程序执行中怎样处理:
1、SIGINT:中断信号,通常由用户通过Ctrl+C产生。假如"Hello"程序正在执行,吸取到SIGINT,它可能会捕捉这个信号并执行一个清理操纵(假如有信号处理函数设置),大概默认操纵是终止程序。
2、SIGHUP:挂起信号,通常在用户退出登录时发送。假如"Hello"程序与终端有关,它可能会处理SIGHUP以优雅地关闭。
3、SIGTERM:终止信号,由操纵系统或另一个进程发送,用于请求程序终止。"Hello"程序可以设置一个信号处理函数来执行清理工作,否则它会立即终止。
4、SIGSEGV:段错误信号,当程序试图访问其内存空间中未分配(或不允许)的部分时触发。"Hello"程序默认环境下不会处理SIGSEGV,程序将异常终止。
5、SIGFPE:浮点异常信号,当程序执行了非法的浮点运算时触发。假如"Hello"程序中包罗浮点运算,且出现错误,这个信号可能会被触发。
6、SIGABRT:由进程内的abort系统调用触发的信号。假如"Hello"程序调用了abort,它将吸取SIGABRT信号并终止。
7、SIGALRM:由alarm函数设置的定时器信号。假如"Hello"程序使用alarm设置了定时器,定时器到期时将产生SIGALRM。
[*]SIGCHLD:子进程结束时发送给父进程的信号。假如"Hello"程序创建了子进程,它可能需要处理SIGCHLD来跟踪子进程的状态。
正常运行
图6.6.1 正常运行
输入ctrl-c,停止
图6.6.2 输入ctrl-c
输入ctrl-z,停止
图6.6.3 输入ctrl-z
乱按并回车,无影响
图6.6.4 乱按并回车
Ctrl-z然后fg,停止然后继续
图6.6.5 Ctrl-z然后fg
Ctrl-z 然后ps 然后kill -9%1 然后ps,停止,然后杀死程序
图6.6.6 Ctrl-z 然后ps 然后kill -9%1 然后ps
Ctrl-z然后ps 然后jobs
图 6.6.7 Ctrl-z然后ps 然后jobs
Ctrl-z然后pstree
图6.6.8 pstree 图6.6.9 pstree
图6.6.10 pstree
6.7本章小结
本章介绍了进程的概念和作用,Shell-bash的作用,以及fork进程创建过程、execve过程,描述了进程的执行和hello的异常和信号处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地点空间
逻辑地点(Logical Address):
逻辑地点是由程序产生的地点,通常是在编译时确定的。在"Hello"程序中,逻辑地点可能在编译阶段被确定,并在程序的代码和数据段中使用这些地点来访问变量和指令。
线性地点(Linear Address):
线性地点是在没有分页机制的内存管理下,程序产生的地点。在现代操纵系统中,线性地点通常指的是虚拟地点空间中的地点。"Hello"程序在执行时,其逻辑地点被转换为线性地点,这个转换过程可能涉及到内存分页和页表查找。
虚拟地点(Virtual Address):
虚拟地点是程序在运行时实际使用的地点,它由操纵系统的内存管理单位(MMU)管理。操纵系统为每个进程(包括"Hello"程序)分配一个独立的虚拟地点空间。当"Hello"程序访问其数据或代码时,它实际上是在访问虚拟地点。
物理地点(Physical Address):
物理地点是实际存储在内存芯片上的地点。在"Hello"程序执行期间,虚拟地点最终需要被转换为物理地点,以便访问实际的内存单位。这个转换过程由操纵系统的MMU负责,通过查找页表来实现。
结合"Hello"程序来说明这些概念:
1、当"Hello"程序编译后,它包罗了逻辑地点,这些地点在程序的代码和数据段中用于访问资源。
2、当程序被加载到内存中执行时,操纵系统会为它创建一个虚拟地点空间,并将程序的逻辑地点映射到这个空间中的虚拟地点。
3、CPU通过虚拟地点来访问程序的指令和数据,这个地点是程序可以直接使用的。
4、操纵系统的MMU将虚拟地点转换为线性地点,假如启用了分页机制,线性地点会进一步转换为物理地点。
5、最终,物理地点被用来访问实际的硬件内存,以执行程序指令或读写数据。
在现代操纵系统中,虚拟内存技术使得每个进程都有本身独立的地点空间,MMU负责在虚拟地点和物理地点之间举行转换,这个过程对进程是透明的。这样,纵然多个进程在物理内存中共享相同的区域,它们也相互隔离,由于每个进程都有本身的虚拟地点空间。
7.2 Intel逻辑地点到线性地点的变更-段式管理
在Intel架构中,段式管理是一种内存管理技术,它使用段寄存器和段选择器来将逻辑地点(也称为段内偏移量)转换为线性地点。以下是段式管理中逻辑地点到线性地点变更的基本过程:
1、段基址(Base Address):
每个段都有本身的基址,这是段在物理内存中的起始位置。段基址存储在段寄存器中,如CS(代码段寄存器)、DS(数据段寄存器)、ES、FS、GS和SS(堆栈段寄存器)。
2、段选择器(Segment Selector):
段选择器包罗段的索引和访问权限等信息。它指向全局描述符表(GDT)或局部描述符表(LDT)中的一个描述符,这些描述符包罗了段的元数据,包括段基址、段界限和访问权限。
3、逻辑地点(Segment Offset):
逻辑地点是程序代码中使用的地点,通常是指令中的立即数或变量的偏移量。在段式管理中,逻辑地点实际上是段内的偏移量。
4、段寄存器加载:
当程序访问一个段时,相应的段寄存器会被加载,包罗段选择器。
5、访问描述符表:
CPU使用段选择器中的索引来访问GDT或LDT,获取对应的段描述符。
6、计算线性地点:
线性地点是通过将段基址与逻辑地点相加得到的。公式为:Linear Address = Segment Base Address + Segment Offset。
7段界限检查:
在将逻辑地点转换为线性地点后,CPU会检查生成的线性地点是否超出了段界限。假如超出,将引发一个段越界异常。
8、权限检查:
CPU还会检查当前的访问权限,确保执行的操纵(如读、写)是允许的。
7.3 Hello的线性地点到物理地点的变更-页式管理
在现代计算机系统中,页式管理(或分页)是常用的内存管理机制,用于将线性地点空间中的线性地点转换为物理地点。以下是在页式管理下,"Hello"程序的线性地点到物理地点变更的基本过程:
1、页表(Page Table):
操纵系统为每个进程维护一张页表,这张表将虚拟地点空间中的页映射到物理内存中的页帧。
2、线性地点的构成:
线性地点由页号(Page Number)和页内偏移(Offset)组成。页号用于索引页表,页内偏移用于在页内定位具体的字节。
3、页目次(Page Directory):
在多级页表布局中,页目次是页表的最高层,包罗页表的地点。CPU中的页目次基址寄存器(如CR3)指向当进步程的页目次。
4、转换过程:
当"Hello"程序访问其地点空间中的某个线性地点时,CPU起首使用线性地点中的页号来索引页目次。
页目次项(PDE)包罗了相应页表的物理地点或指向下一级页目次的指针。
CPU接着使用页号的下一部分来索引页表,找到页表项(PTE)。
页表项包罗了物理页帧的基地点以及一些状态信息,如是否存在(Present)标志。
5、计算物理地点:
物理地点是通过将页表项中的页帧基地点与线性地点中的页内偏移量相加得到的。公式为:Physical Address = Page Frame Base Address + Offset。
6、内存访问:
假如页表项中的存在标志被设置,CPU将允许访问,并将线性地点转换为物理地点以访问物理内存。
假如页表项中的存在标志未被设置,表现该页不在物理内存中,将触发缺页异常(Page Fault),操纵系统需要将缺失的页从磁盘加载到内存中,并更新页表。
7、权限检查:
页表项还包罗权限信息,CPU会检查当进步程是否有权访问该页。
通过页式管理,操纵系统可以或许实现内存的虚拟化,允许每个进程拥有本身的连续虚拟地点空间,同时物理内存可以黑白连续的。页式管理还支持内存掩护和安全,由于每个页表项都可以独立设置权限。"Hello"程序在执行时,它的线性地点通过页表转换为物理地点,使得程序可以或许访问到物理内存中的指令和数据。
7.4 TLB与四级页表支持下的VA到PA的变更
在现代处理器中,虚拟地点(VA)到物理地点(PA)的变更通常涉及一个多级页表布局和转换后备缓冲器(TLB,也称为快表)。以下是在四级页表支持下的VA到PA变更的基本过程:
1、虚拟地点布局:
虚拟地点被分为多个部分,包括多级索引(在四级页表中为四级)、页表项(PTE)和页内偏移。
2、四级页表布局:
页表布局是分层的,通常有四级:页全局目次(PGD)、页上级目次(PUD)、页中心目次(PMD)和页表(页表项PTE)。
3、TLB(Translation Lookaside Buffer):
TLB是一个高速缓存,用于存储最近或频仍访问的虚拟地点到物理地点的映射条目。当虚拟地点需要转换时,CPU起首检查TLB。
4、TLB查找:
CPU使用虚拟地点中的索引部分来查找TLB。假如TLB掷中,即找到了对应的条目,CPU可以直接获取物理地点,并将其与页内偏移组合。
5、页表遍历:
假如TLB未掷中,CPU需要遍历页表。起首使用虚拟地点中的最高级别索引来访问页全局目次(PGD)。
接着使用下一级索引来访问页上级目次(PUD),依此类推,直到找到页表项(PTE)。
6、页表项检查:
页表项(PTE)包罗了物理页帧的基地点以及状态信息,如是否存在标志。CPU检查PTE以确定页是否有效。
7、计算物理地点:
假如页表项有效,CPU将页表项中的页帧基地点与虚拟地点中的页内偏移相加,得到完整的物理地点。
8、TLB更新:
一旦物理地点被解析,该虚拟地点到物理地点的映射可能会被加载到TLB中,以便未来的访问可以更快地举行。
9、缺页异常处理:
假如在页表遍历过程中发现页表项不存在或无效,将触发缺页异常。操纵系统将处理这个异常,将缺失的页加载到物理内存中,并更新页表项。
四级页表布局提供了巨大的虚拟地点空间,可以有效地管理大量内存,而TLB的使用显著减少了页表遍历的需要,从而加快了地点转换的速度。这种结合使用TLB和多级页表的方法是现代处理器实现高效内存管理的关键技术之一。
图7.4.1 上下文切换
7.5 三级Cache支持下的物理内存访问
在现代计算机体系布局中,三级缓存(L3 Cache)作为CPU与主内存(物理内存)之间的一个高速缓冲层,其目标是通过存储最近或频仍访问的数据来加速数据的访问速度,从而减少访问主内存的延迟。以下是三级缓存支持下物理内存访问的基本过程:
1、请求发起:当CPU需要访问数据时,起首会查看最靠近CPU的L1缓存。L1缓存由于体积小、速度快,通常存放着最常用的数据。
2、L1 Cache掷中或缺失:
掷中(Cache Hit):假如所需数据在L1缓存中找到,CPU直接使用这些数据,无需继续向下查找,这是最抱负的环境。
缺失(Cache Miss):假如L1中没有所需数据,CPU接着会检查L2缓存,L2相比L1更大但稍慢一些。
3、L2 Cache掷中或缺失:
掷中:假如数据在L2中,CPU将数据取回并可能将其复制到L1以备后续更快访问。
缺失:假如L2也没有所需数据,查询流程会继续到L3缓存。
4、L3 Cache操纵:
掷中:L3作为更大但速度仍然远高于主内存的缓存层,假如数据在这里找到,它会被传递给L1或L2(根据计划),然后再到CPU。这减少了对更慢的主内存的依靠。
缺失:假如L3中也没有数据,请求最终会到达主内存。主内存会将所需数据传送到L3,L2以致L1(假如有空间且计谋允许),末了到CPU。
5、主内存访问:在L3也未掷中的环境下,数据会从主内存(DRAM)中读取,这个过程比访问任何级别的缓存都要慢得多。数据在被CPU使用后,可能会被加载到各级缓存中以便未来快速访问。
6、更新与划一:在数据被修改后,为了维护缓存与主内存间的数据划一性,会采用诸如写直达(Write Through)、写回(Write Back)等计谋,而且在多核系统中还需要考虑缓存划一性协议,确保各个CPU核心看到的数据是划一的。
整个访问流程表现了分级缓存的计划理念,即通过多级缓存系统逐级筛选,尽可能地减少对慢速主内存的直接访问,从而进步整体系统性能。
7.6 hello进程fork时的内存映射
当一个进程执行fork()系统调用时,操纵系统会创建一个新的进程,这个新进程称为子进程,它几乎是父进程的一个完全复制,包括父进程的内存空间。关于hello进程在执行fork()时的内存映射,可以概述如下:
1、写时复制(Copy-on-Write):fork()之后,父子进程共享相同的内存映射,这意味着他们指向相同的物理内存页面。这种机制称为写时复制(Copy-on-Write, COW)。直到其中一个进程尝试修改共享页面上的数据时,才会为该进程复制一份新的页面,以保持数据的隔离性。这样可以高效地利用内存资源,制止不须要的数据复制。
2、内存映射文件:假如父进程中存在内存映射的文件(比如程序代码段、动态链接库等),这些映射也会被复制到子进程中。同样地,这些映射在开始时也是共享的,遵循COW原则。
3、堆和栈:父进程的堆和栈区域也会被复制到子进程中,同样也是在实际写入时才分离。这意味着子进程会继承父进程的堆栈状态,但任何后续的堆分配或栈增长都将独立举行,不会影响对方。
4、只读数据段:程序的只读数据段(比方初始化的全局变量、字符串常量等)在父子进程中也是共享的,直到有写入尝试时才会复制。
5、权限与属性:只管内存布局相同,但每个进程都有本身的独立内存空间视图,包括权限设置(如读、写、执行)。这意味着纵然父子进程共享某些页面,在访问控制上也是独立的。
总结来说,hello进程在执行fork()时,其内存空间近乎完整地复制给了子进程,但实际上大多数数据并未立即复制,而是通过写时复制机制来延迟和优化实际的内存使用。这种方式既保证了子进程可以或许拥有与父进程相同的初始环境,又进步了效率,制止了不须要的资源斲丧。
7.7 hello进程execve时的内存映射
当一个进程执行execve()系统调用时,它的目标通常是加载并运行一个新的程序,如替换当进步程的hello程序为另一个程序。在这个过程中,进程的内存映射会发生显著变化,主要包括以下几个方面:
1、代码段和数据段的替换:execve()会导致当进步程的代码段、数据段(包括全局变量和静态变量)、BSS段以及堆栈段的内容被即将执行的新程序的相应部分替换。这意味着hello程序的二进制代码和数据将被卸载,取而代之的是新程序的代码和数据。
2、动态链接与库加载:假如新程序依靠动态链接库,execve()过程会解析这些依靠,并将须要的动态库加载到进程的地点空间中。这涉及到重定位、符号解析等步骤,以确保新程序可以或许正确地引用外部函数和变量。
3、堆和栈的初始化:虽然execve()不直接清空堆区(由于可能有共享库的全局数据需要保留),但它会重新初始化栈,为新程序准备好运行环境。栈中会放置新程序的启动参数、环境变量指针以及返回到内核的地点等信息。
4、内存映射的创建:对于内存映射文件或共享对象,execve()会根据新程序的需求创建或调解相应的内存映射。这可能涉及加载共享库或其他需要映射到进程地点空间的文件。
5、环境变量的传递:execve()允许传递新的环境变量给新程序,大概继承调用者的环境变量。这些环境变量在进程的内存映射中占有特定位置,并可供新程序通过标准API访问。
6、Text, Data, BSS段的分离:新程序的内存布局会被重新组织,其中文本段(代码)通常标志为只读,数据段和BSS段根据初始化与否分别设置为可读写或初始化为零。
总之,execve()不光仅加载了一个新的程序到内存中,还彻底改变了进程的内存布局和内容,准备好了所有须要的环境以便新程序可以或许从其入口点开始执行。原hello进程的几乎所有痕迹都会被清除,仿佛进程“重生”为一个新的程序。
7.8 缺页故障与缺页中断处理
缺页故障(Page Fault)与缺页中断处理是现代操纵系统中虚拟内存管理的关键机制,它们使得系统可以或许在有限的物理内存中运行需要更多内存资源的程序。下面是这两个概念的扼要说明:
缺页故障(Page Fault)
缺页故障发生在以下情境下:
1、访问未加载的页面:当一个进程尝试访问一个虚拟地点,而该地点对应的物理页面当前并未加载到内存中时,就会发生缺页故障。这可能是由于该页面从未被加载过,大概之前被换出到外存(如硬盘上的交换空间)以腾出物理内存供其他页面使用。
2、权限辩论:假如一个进程试图以错误的访问权限(比如写一个只读页面)访问一个页面,某些系统也可能报告为缺页故障。
缺页中断处理
一旦发生缺页故障,操纵系统会介入执行以下处理流程:
1、保存现场:CPU暂停当进步程的执行,将当前的上下文(包括指令计数器PC的值、各种寄存器的状态等)保存到内核栈或任务状态段,以确保后续能恢复执行。
2、检查原因:操纵系统检查缺页的原因,确认是否确实需要从外存加载页面,或是处理权限题目。
3、页面置换(假如须要):假如物理内存已满,操纵系统需要根据页面置换算法(如LRU、LFU、FIFO等)选择一个牺牲页面将其换出到外存,为新页面腾出空间。
4、调入所需页面:从外存(如磁盘上的交换分区或分页文件)读取缺少的页面到物理内存中,并更新页表和TLB(Translation Lookaside Buffer,快表)以反映新的内存映射关系。
5、恢复现场并继续执行:完成页面加载后,操纵系统恢复之前保存的上下文,使进程可以或许从引发缺页故障的那条指令开始重新执行。此时,由于所需页面已经在内存中,故指令可以乐成执行,不再触发缺页。
通过这样的机制,操纵系统可以或许有效地管理物理内存,支持虚拟地点空间大于物理内存的程序运行,而且在需要时自动管理页面的加载与换出,进步了资源的利用率和系统的整体性能。
7.9动态存储分配管理
Printf会调用malloc,请简述动态内存管理的基本方法与计谋。
动态存储分配管理是编程中一种灵活的内存使用方式,允许程序在运行时按需请求和开释内存。这与静态内存分配不同,后者在编译时就已经确定了内存的分配。动态内存管理主要涉及以下几个基本方法与计谋:
基本方法:
1、malloc(): 分配一块连续的内存区域,其大小由用户指定。返回一个指向这块内存起始位置的指针,假如分配失败则返回NULL。
2、calloc(): 雷同于malloc(),但会将分配的内存初始化为零。
3、realloc(): 调解先前通过malloc()或calloc()分配的内存块的大小。假如需要扩大内存,可能需要移动原有的内存内容到新的位置;假如缩小,则开释多余的部分。
4、free(): 开释之前通过malloc()、calloc()或realloc()分配的内存。开释后,这块内存可以被操纵系统再次分配给其他进程使用。
计谋:
1、内存池: 预先分配一大块内存作为内存池,然后从池中快速分配和采取小块内存,减少每次分配/开释调用系统调用的开销。
2、碎片整理: 通过不同的分配和采取计谋(如同伴系统、slab分配器)尽量减少内存碎片,进步内存利用率。
3、写时复制(Copy-on-Write): 在fork()等操纵中,最初让父进程和子进程共享同一份物理内存,只有当任一进程试图修改时,才真正复制一份给修改者,节省内存并加快进程创建速度。
4、垃圾采取: 在一些高级语言中(如Java、Python),有自动垃圾收集机制,自动追踪不可达的内存并采取,减轻程序员管理内存的负担。
假如printf()格式化字符串中包罗变长参数(如 %s、%p 或 %d 等),编译器生成的代码在处理这些参数时可能会间接导致动态内存分配,尤其是在构造格式化输出字符串时。不外,这部分内存管理通常是由运行时库负责,而不是直接由printf()函数处理。
7.10本章小结
本章简朴介绍了hello的存储器地点空间,Intel逻辑地点到线性地点的变更-段式管理,Hello的线性地点到物理地点的变更-页式管理,TLB与四级页表支持下的VA到PA的变更,三级Cache支持下的物理内存访问,hello进程fork时的内存映射,hello进程execve时的内存映射,缺页故障与缺页中断处理和动态存储分配管理
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux的I/O设备管理方法核生理念是“齐备皆文件”。这意味着无论是硬盘、键盘、显示器还是网络接口,都被抽象成文件的情势,这样就可以使用统一的文件操纵接口(如open(), read(), write(), close()等)来对它们举行操纵。这种方法极大地简化了设备管理,进步了系统的可扩展性和兼容性。
设备的模型化:文件
1、设备文件:在Linux中,每个设备对应一个或多个设备文件,位于/dev目次下。这些设备文件可以像普通文件一样被打开、读取、写入和关闭。设备文件分为两类:字符设备文件(提供无缓冲的、按字节流方式访问的设备,如键盘、串口)和块设备文件(提供缓冲的、按块访问的设备,如磁盘、光驱)。
2、设备驱动:设备驱动是操纵系统内核的一部分,提供了硬件设备与内核及上层应用之间的接口。驱动程序实现了设备的初始化、读写操纵、电源管理等须要功能,并通过一组标准的接口函数(即设备模型中的方法)与内核和其他部分通信。
设备管理:Unix I/O接口
Unix/Linux的I/O接口主要包括以下几种系统调用:
open():打开一个文件或设备,返回一个文件描述符(file descriptor),用于后续的读写操纵。
read():从文件描述符关联的文件或设备中读取数据。
write():向文件描述符关联的文件或设备中写入数据。
close():关闭一个已打开的文件或设备,开释资源。
ioctl():输入/输出控制命令,用于执行设备特定的操纵,如配置设备参数。
Unix I/O模型
Unix/Linux还支持多种I/O模型,以顺应不同的I/O需求和性能要求,主要包括:
1、壅闭I/O:默认模式,调用会不绝等待直到操纵完成。
2、非壅闭I/O:调用立即返回,无论操纵是否完成。
3、I/O多路复用:允许同时监控多个文件描述符,等待任何一个准备好举行I/O操纵。
4、信号驱动I/O:通过信号机制关照应用程序何时可以举行I/O操纵。
5、异步I/O (AIO):真正的异步模型,发起I/O请求后,完全不壅闭,内核完成后通过回调关照应用。
通过这些接口和模型,Linux系统可以或许高效、灵活地管理各类设备,使得设备的使用对于用户和开辟者而言变得透明且划一。
8.2 简述Unix IO接口及其函数
Unix I/O接口是Unix和类Unix系统(如Linux)中用于执行输入/输出操纵的核心组件,它允许用户空间的应用程序与操纵系统内核举行交互,从而读取或写入数据到文件、设备或网络套接字等。Unix I/O接口的计划遵循“齐备皆文件”的哲学,这意味着无论是普通文件、设备还是网络连接,都可通过相同的文件描述符和系统调用来举行操纵。以下是一些关键的Unix I/O接口函数及其简述:
1、open():
作用: 用于打开一个文件或设备。它吸取一个路径名和一些标志(如读、写权限),乐成时返回一个文件描述符(一个非负整数)。
2、read():
作用: 从已打开的文件或设备读取数据。需要提供文件描述符、一个缓冲区来存放读取的数据,以及要读取的字节数。
3、write():
作用: 向已打开的文件或设备写入数据。需要提供文件描述符、要写入的数据缓冲区,以及数据的字节数。
4、lseek():
作用: 在文件或设备中移动读写位置。常用于定位到特定偏移量举行读写操纵。
5、close():
作用: 关闭一个已打开的文件或设备,开释系统资源。
6、fcntl():
作用: 用于对文件描述符执行多种控制操纵,如更改文件访问模式、获取或设置文件锁等。
8.3 printf的实现分析
printf:
图8.3.1 printf
vsprintf:
图8.3.2 vsprintf
在Unix-like系统中,从使用vsprintf生成格式化的字符串到通过write系统调用将信息输出到标准输出或文件,整个过程涉及几个关键步骤,最终触达硬件层面完成实际的I/O操纵。这个流程可以分为以下几个阶段,特别是在较老的系统中使用int 0x80作为系统调用入口,而在现代系统(如Linux)中可能使用syscall指令:
1、使用vsprintf格式化字符串:
vsprintf是一个库函数,它担当一个格式字符串和一个变长参数列表,按照指定格式将数据格式化成字符串,并写入到一个字符数组中。这一步发生在用户空间,不涉及系统调用。
2、调用write系统调用:
用户程序通过调用write函数,意图将格式化后的字符串输出。write是一个系统调用,其参数通常包括文件描述符(比方,1代表标准输出)、指向要写入数据的指针,以及要写的字节数。
3、陷入内核(Trap or System Call):
当执行到write这样的系统调用时,用户程序会执行一个特殊的指令来“陷入”(或切换)到内核态。在较旧的x86系统上,这是通过执行int 0x80指令实现的;而在现代Linux系统中,通常使用syscall指令,它提供了更高效和直接的方式进入内核并执行系统调用。
4、系统调用处理:
一旦进入内核态,系统调用处理程序会根据系统调用号(对于write来说,这是一个固定的编号)来确定要执行的操纵。它会验证参数的有效性,然后执行实际的写操纵。在这个过程中,内核会检查权限、执行缓冲区管理等。
5、硬件交互:
最终,内核会通过适当的驱动程序与硬件交互,将数据从内核缓冲区传输到指定的输出设备(如显示器或磁盘)。这一过程可能涉及DMA(直接内存访问)或其他低级I/O技术。
6、返回用户空间:
系统调用完成后,控制权返回给用户空间程序,同时返回一个表现调用结果的错误码(通常是0表现乐成)。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
在操纵系统中,getchar函数常用于从标准输入(通常是键盘)读取一个字符。当涉及到键盘中断(如用户按下Ctrl+C产生的中断)的处理时,整个流程变得更加复杂,由于它不光涉及到字符的获取,还包括了中断处理和同步/异步事件的管理。下面是对一个简化的getchar实现的分析,特别是考虑到键盘中断的处理:
1、键盘中断触发与相应:
用户按下键盘按键,硬件键盘控制器生成扫描码并通过中断请求线(IRQ)关照CPU。
CPU相应中断,保存当前任务上下文,然后执行中断服务例程(ISR)。
ISR读取扫描码,根据系统键盘布局映射将其转换为ASCII码或保留为特殊控制码,并将结果存入键盘缓冲区。假如缓冲区满,则根据系统计谋处理(如抛弃、等待等)。
2、getchar函数逻辑:
应用程序调用getchar函数意在读取一个字符。
getchar内部通常通过系统调用(如read)与操纵系统内核交互,请求从标准输入(键盘)读取数据。
此read调用是壅闭式的,意味着假如没有数据可读,调用线程将被挂起,直到有数据可用或发生中断。
3、处理中断与继续执行:
假如在read执行期间发生键盘中断(如Ctrl+C),内核会中断read调用,设置errno为EINTR,并返回-1,表现此次系统调用因中断而未完成。
应用程序在捕获到EINTR错误时,可以选择重试read调用,继续等待键盘输入,或根据程序逻辑做出其他相应。
4、字符读取与返回:
一旦有字符(包括回车符\n)可读,read调用乐成返回,将字符送至调用者。
对于标准getchar实现,它通常会直接返回读取到的第一个字符,而非比及回车。但特定应用可能自行添加逻辑,如等待用户按下回车后再返回。
综上所述,getchar函数的执行不光仅是简朴的数据读取,它深刻地嵌入了操纵系统对中断处理、输入缓冲管理以及用户交互的复杂机制中。
8.5本章小结
本章节深入探究了系统级I/O的核心概念以及Unix环境下的I/O基础,夸大了I/O操纵在系统功能实现中的根本性作用,并指出把握I/O机制对于深入理解其他系统原理至关告急。通过对标准化输入输出函数printf与getchar的过细分析,不光揭示了它们的工作原理,还彰显了这些基本函数怎样桥接用户与操纵系统之间的交互,从而为后续的系统学习奠基了坚固的基础。
(第8章1分)
结论
在计算机系统的世界里,"Hello"程序的旅程就像一部精彩绝伦的史诗,布满了奥妙与神奇。让我们跟随"Hello"程序的脚步,一起探索这段令人赞叹的旅程。
预处理阶段:这是"Hello"的诞生之地。想象一下,"hello.c"文件就像一个布满潜力的婴儿,等待着被赋予生命。预处理器就像一位慈祥的导师,它辨认并执行宏界说的替换,处理文件包罗指令,为"Hello"程序的诞生打下坚固的基础。这一阶段,"Hello"程序从一个简朴的文本文件,蜕变为一个更加精炼的中心文件"hello.i",仿佛是一次神奇的变形。
编译阶段:接下来,"Hello"程序进入编译阶段,这里就像是它的青少年时期,布满了发展与变化。编译器这位严酷的教练,将"hello.i"转换成汇编语言的"hello.s"文件,这是"Hello"程序第一次以呆板可理解的情势出现。编译器不光处理C语言的数据范例和操纵,还确保了代码的模块化和可重用性,让"Hello"程序变得更加健壮和灵活。
汇编阶段:随着"Hello"程序的发展,它来到了汇编阶段,这里就像是它的成年礼。汇编器这位技艺高超的工匠,将汇编语言翻译成呆板语言二进制程序,生成"hello.o"这个可重定位目标文件。这一阶段,"Hello"程序进一步蜕变,从一个抽象的概念,变成了实着实在的呆板指令,准备着踏上执行的舞台。
链接阶段:末了,在链接阶段,"Hello"程序完成了它的最终转变。链接器这位伟大的指挥家,将"hello.o"与系统库中的代码链接起来,创造出最终的可执行文件。这一过程就像是将不同的乐器组合成一曲和谐的交响乐,每一个部分都发挥着它独特的作用,共同创造出美妙的音乐。
运行时:当"Hello"程序作为一个进程被加载到内存中,CPU这位伟大的指挥家开始控制其指令的执行。它根据吸取的信号举行异常处理,确保"Hello"程序可以或许顺畅地运行。当"Hello"进程终止后,该实例被销毁,内存被采取,"Hello"程序完成了它的生命周期,就像一位完成了使命的好汉,优雅地退出了舞台。
这段旅程不光仅是技术的展示,更是计算机系统计划与实现的一次深刻表现。它展示了计算机系统资源管理的高效性和灵活性,也让我们对计算机系统有了更深的理解和感悟。"Hello"程序的每一次转变,都是计算机科学中的一次小小的古迹,它们共同编织成了这个布满奥妙与神奇的计算机世界。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.c: 源文件
hello.i: 预处理后生成的文件
hello.s: 编译后生成的文件
hello.o: 汇编后生成的文件,为可重新定位目标的程序
hello: 可执行目标文件
hello.asm: hello.o的反汇编生成的文件
hello.elf: hello.o的elf格式文件
hello345.asm: hello的反汇编生成的文件
hello2.elf: hello的elf格式文件
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的册本与网站等
Randal E. Bryant,David O’Hallaron,《深入理解计算机系统》(原书第三版),北京:机械工业出书社,2021:10-1.
https://www.cnblogs.com/pianist/p/3315801.html
GDT(全居描述符表)和LDT(局部描述符表)
https://blog.csdn.net/genghaihua/article/details/89450057
x86-64 汇编:寄存器和过程调用约定
https://blog.csdn.net/qq_34908601/article/details/123772569
(参考文献0分,缺失 -1分)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]