哈工大计算机系统期末大作业-程序人生

06-28 1141阅读

哈工大计算机系统期末大作业-程序人生
(图片来源网络,侵删)

计算机系统

 

大作业

题     目  程序人生-Hellos 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变量

  1. 寄存器变量:

%rdi, %rsi, %rdx, %rax, %rcx: 这些寄存器用于传递函数参数和返回值,以及临时存储操作数。

  1. 栈上局部变量:

-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。

  1. 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分)


参考文献

为完成本次大作业你翻阅的书籍与网站等

[1]  Randal E. Bryant,David O’Hallaron,《深入理解计算机系统》[M](原书第三版),北京:机械工业出版社,2021:10-1.

[2]  https://www.cnblogs.com/pianist/p/3315801.html

[3] GDT(全居描述符表)和LDT(局部描述符表)

https://blog.csdn.net/genghaihua/article/details/89450057

[4] x86-64 汇编:寄存器和过程调用约定

https://blog.csdn.net/qq_34908601/article/details/123772569

(参考文献0分,缺失 -1分)

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]