• 哈工大2024春季计算机系统大作业(水平有限,仅供参考)


    摘  要

    作为计算机系统课程的结业论文,本文章基于Computer Systems: A Programmer’s Perspective (3rd ed.),通过对一个简单的程序Hello在Linux环境下的生命周期的分析,论述其从hello.c经过预处理、编译、汇编、链接等一系列操作生成可执行文件hello,再通过程序对进程的管理、内存空间的分配、信号和异常的处理、对 I/O 设备的调用等环节彻底解释hello从创建到结束的过程,进而加深对计算机系统的理解。

    关键词:程序生命周期;计算机系统;hello程序;预处理;编译;汇编;链接;                    

    目  录

    第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简介

    1.1.1 P2P: From Program to Process
    • 预处理阶段:Hello.c源程序首先经过预处理器(cpp)进行预处理,根据以字符#开头的命令,读取系统头文件的内容并将其直接插入程序文本中,修改原始的C程序,生成另一个通常以.i作为文件扩展名的C文件,即hello.i。
    • 编译阶段:接着,hello.i文件经过编译器(ccl)进行编译,将其转换为汇编语言代码,生成hello.s文件。
    • 汇编阶段:随后,hello.s文件经过汇编器(as)进行汇编,将其转换为机器语言命令,并打包为可重定位的目标程序,即hello.o文件。
    • 链接阶段:最后,链接器(ld)将hello.o文件和引用到的库函数链接在一起,生成可执行目标文件hello。

    1.1.2 020: From Zero-0 to Zero-0
    • 加载与运行:当在shell中运行hello程序时,操作系统为进程分配虚拟空间,将程序从磁盘加载到物理内存中,并通过mmap实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一映射关系。此时,hello程序从无到有。
    • 执行:CPU为该进程分配时间片,执行hello程序对应的逻辑控制流。
    • 终止与回收:当hello程序执行完毕后,操作系统回收进程,释放属于hello程序的一切资源,包括虚拟内存空间等。此时,hello程序从有到无,实现了从0到0的转变,即020。

    1.2 环境与工具

    1.2.1 硬件环境

    X64 CPU;2GHz;2G RAM;256GHD Disk

    1.2.2 软件环境

    Windows 11 64位;Visual Studio 2022;Ubuntu 20.04 LTS 64位

    1.2.3 开发与调试工具

    Visual Studio 2022;gdb、edb等

    1.3 中间结果

    1. 1 论文撰写过程中生成的中间文件与功能备注

    文件名

    备注

    hello.c

    源代码文件

    hello.i

    预处理后的文本文件

    hello.s

    编译后的汇编文件

    hello.o

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

    hello

    链接后的可执行文件

    hello.elf

    hello.o生成ELF文件

    hello.asm

    反汇编hello.o的反汇编文件

    hello.elf2

    hello可执行文件生成的ELF文件

    hello.asm2

    反汇编hello得到的反汇编文件

    1.4 本章小结

            本章我们主要交代了hello的P2P和020过程,给出了论文研究时的环境与工具以及中间生成的文件信息。


    第2章 预处理

    2.1 预处理的概念与作用

    2.1.1预处理的概念

            预处理是编译器处理源代码的第一个阶段,它读取源代码文件(通常是.c、.cpp、.java等文件),并根据预处理指令(如#include、#define等)对源代码进行初步的修改或替换。预处理完成后,生成一个新的源代码文件(或直接在内存中处理),该文件将作为编译器的下一个阶段(编译阶段)的输入。

    2.1.2预处理的作用
    • 包含头文件:通过#include指令,预处理器会将指定的头文件(通常是.h文件)的内容插入到源代码中。这允许程序员将常用的代码段(如函数声明、宏定义等)放在头文件中,并在多个源文件中重复使用。
    • 宏定义与替换:#define指令用于定义宏,宏可以是常量、函数原型或复杂的代码块。在预处理阶段,预处理器会将源代码中的宏替换为相应的定义。这有助于简化代码、提高代码的可读性和可维护性。
    • 条件编译:#if、#ifdef、#ifndef、#else、#elif和#endif等指令允许程序员根据特定的条件(如操作系统类型、编译器版本等)选择性地编译代码。这有助于编写跨平台的代码,确保代码在不同环境下都能正确运行。
    • 注释处理:虽然注释不是预处理指令,但预处理器会处理源代码中的注释。在C和C++中,单行注释以//开头,多行注释以/*开头并以*/结束。预处理器会移除这些注释,以便在后续的编译阶段中不会包含它们。
    • 其他预处理指令:除了上述常见的预处理指令外,还有一些其他指令(如#pragma)允许程序员向编译器提供特定的指令或建议,以优化代码或满足特定需求。

    2.2在Ubuntu下预处理的命令

    图2.1 cpp预处理命令效果

    直接用cpp对hello.c文件进行预处理,会直接显示处理结果,因为需要使用重定向保存处理结果至hello.i(预处理之后的文件)中,具体如下:

    图2.2 cpp预处理后保存至hello.i

    2.3 Hello的预处理结果解析

    对于预处理结果,我们可以直接使用VS打开hello.s文件查看,具体如下:

    图2.3 hello.i中部分文件

    图2.4 hello.i中部分宏定义

    图2.5 hello.i中部分函数声明

    图2.6 hello.i中的原始C代码部分

    2.4 本章小结

    本章节我们回顾了预处理器的概念与主要作用,并对hello.c进行了预处理,最后对hello.i文件内容进行了简单分析。我们现在所得到的hello.i文件在下一章节会作为编译器(ccl)的输入文本。
    第3章 编译

    3.1 编译的概念与作用

    3.1.1 编译的概念

    编译器(ccl)读取预处理后的文件,并将其转换为汇编语言源代码(通常是.s扩展名)。

    3.1.2 编译的作用
    • 语法和语义分析:编译器读取hello.i文件,并检查其语法和语义。它会确保源代码符合编程语言的规则,并识别出所有的变量、函数、操作符等。
    • 生成汇编代码:基于语法和语义分析的结果,编译器将源代码转换为汇编语言代码,并保存在hello.s文件中。这个过程将高级语言的构造(如变量声明、函数调用、控制流语句等)转换为汇编指令。
    • 优化:在这个阶段,编译器可能会尝试优化生成的汇编代码。优化可能包括消除无用的代码、简化复杂的表达式、重新组织代码结构等,以提高最终生成的可执行文件的性能

    3.2 在Ubuntu下编译的命令

    图3.1 将hello.i编译为hello.s的命令

    3.3 Hello的编译结果解析

    3.3.1 汇编代码

    1..file "hello.c"

    2..text

    3..section .rodata

    4..align 8

    5..LC0:

    6..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"   

    7..LC1:

    8..string "Hello %s %s %s\n"

    9..text

    10..globl main

    11..type main, @function

    12.main:

    13..LFB6:

    14..cfi_startproc

    15.endbr64

    16.pushq %rbp

    17..cfi_def_cfa_offset 16

    18..cfi_offset 6, -16

    19.movq %rsp, %rbp

    20..cfi_def_cfa_register 6

    21.subq $32, %rsp

    22.movl %edi, -20(%rbp)

    23.movq %rsi, -32(%rbp)

    24.cmpl $5, -20(%rbp)

    25.je .L2

    26.leaq .LC0(%rip), %rdi

    27.call puts@PLT

    28.movl $1, %edi

    29.call exit@PLT

    30..L2:

    31.movl $0, -4(%rbp)

    32.jmp .L3

    33..L4:

    34.movq -32(%rbp), %rax

    35.addq $24, %rax

    36.movq (%rax), %rcx

    37.movq -32(%rbp), %rax

    38.addq $16, %rax

    39.movq (%rax), %rdx

    40.movq -32(%rbp), %rax

    41.addq $8, %rax

    42.movq (%rax), %rax

    43.movq %rax, %rsi

    44.leaq .LC1(%rip), %rdi

    45.movl $0, %eax

    46.call printf@PLT

    47.movq -32(%rbp), %rax

    48.addq $32, %rax

    49.movq (%rax), %rax

    50.movq %rax, %rdi

    51.call atoi@PLT

    52.movl %eax, %edi

    53.call sleep@PLT

    54.addl $1, -4(%rbp)

    55..L3:

    56.cmpl $9, -4(%rbp)

    57.jle .L4

    58.call getchar@PLT

    59.movl $0, %eax

    60.leave

    61..cfi_def_cfa 7, 8

    62.ret

    63..cfi_endproc

    64..LFE6:

    65..size main, .-main

    66..ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0"

    67..section .note.GNU-stack,"",@progbits

    68..section .note.gnu.property,"a"

    69..align 8

    70..long  1f - 0f

    71..long  4f - 1f

    72..long  5

    73.0:

    74..string  "GNU"

    75.1:

    76..align 8

    77..long  0xc0000002

    78..long  3f - 2f

    79.2:

    80..long  0x3

    81.3:

    82..align 8

    3.3.2 数据

    3.3.2.1 常量

    在编译过程中,如果编译器发现使用了常量,它会直接以符号表中的值替换该常量。这意味着在生成的机器代码中,常量的值会被直接嵌入到指令中。例如:

    6..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" 

    为“用法:hello 学号 姓名 手机号 秒数!”的机器代码,其被存储至内存中。

    3.3.2.2 变量

    编译器将已初始化的全局和静态C变量存储至 .data节,将未初始化的全局和静态C变量存储至 .bss节,而对于局部C变量,运行时被保存至栈中。例如:

    31.movl $0, -4(%rbp) 

    此汇编语句将第一个局部变量i赋初值0,对应至图2.6中的3055行i=0 。

    3.3.3 赋值操作

    赋值语句常用于对局部变量值的传递,对常量的传递等。

    3.3.2.2中所写即为典型的赋值语句,不在赘述。

    需注意movb、movw、movl和movq都为赋值指令,不同在于后缀指明了不同字大小,使用时需与寄存器大小保持一直。其中movl指令默认将目的寄存器的高32位置0。

    3.3.4 算术运算

    此汇编代码中算数运算较为简单,主要有以下几种:

    • 21.subq $32, %rsp //此语句将栈指针上移4字节,实现对栈空间的初始化。
    • 54.addl $1, -4(%rbp) //此语句将局部变量i加一,对应图2.6中3055行中i++部分。

    3.3.5 关系操作

    此汇编代码中关系操作较少,主要有以下几种:

    • 24.cmpl $5, -20(%rbp) //此语句比较局部变量argc与5的大小关系(此前编译器将两个参数都压入栈中,是编译器的一种优化,降低了函数中高频引用所产生的CPU访存时间),对应图2.6中3051行。
    • 56.cmpl $9, -4(%rbp) //此语句比较局部变量i与9的大小关系,对应图2.6中3055行i<10部分。

    3.3.6 数组/指针/结构操作

    对数组的索引相当于在数组首元素地址的基础上通过加索引值乘以数据大小来实现。

    此汇编代码中有根据argv首地址获得argv[1]和argv[2]的数组操作,具体如下:

    33..L4:

    34.movq -32(%rbp), %rax 

    35.addq $24, %rax      

    36.movq (%rax), %rcx

    37.movq -32(%rbp), %rax

    38.addq $16, %rax

    39.movq (%rax), %rdx

    40.movq -32(%rbp), %rax

    41.addq $8, %rax

    42.movq (%rax), %rax

    43.movq %rax, %rsi        //此段代码通过对栈顶元素char* argv[] 的加字节

    44.leaq .LC1(%rip), %rdi //运算,实现了对argv数组四个元素的逐一访问。    

    45.movl $0, %eax

    3.3.7 控制转移

    此汇编代码中的控制转移语句较多,以下分析主要的典型:

    • 24.cmpl $5, -20(%rbp) //此段语句,比较局部变量argc与5的大小,如若

    25.je .L2         //两者相等,则跳转至 .L2处,对应图2.6中3051行。

    • 31.movl $0, -4(%rbp) //此段语句,将局部变量i赋0后直接跳转至 .L3处

    32.jmp .L3         //是一种典型的“跳转至中间”的循环语句编译方法。

    • 56.cmpl $9, -4(%rbp) //此段语句,比较局部变量i与9的大小,如若i <= 9,

    57.jle .L4         //则跳转至 .L4处,对应图2.6中3055行i<10部分。

    3.3.8 函数操作

    此汇编代码中的函数调用较为简单,但其内在内在逻辑需要我们理解:首先,内核shell获取命令行参数和环境变量地址,执行main函数。在main中,需调用其他函数,并为这些函数分配栈空间。调用函数时,借助栈先将返回地址压入,然后设置PC为被调用函数起始地址。返回时,从栈中弹出返回地址,设置PC为该地址。return正常返回后,leave恢复栈空间。具体如下:

    27.call puts@PLT

    29.call exit@PLT

    46.call printf@PLT

    51.call atoi@PLT

    53.call sleep@PLT

    58.call getchar@PLT

    3.4 本章小结

    本章节我们探讨了编译器将预处理后的C程序hello.i翻译成汇编语言的过程,涉及各类数据、算术运算、关系操作、数组操作、控制转移和函数操作等方面。编译器还会优化程序,结果保存在hello.s文件中。该文件将作为下一章节汇编器(as)的输入文本。
    第4章 汇编

    4.1 汇编的概念与作用

    4.1.1 汇编的概念

    汇编是将汇编语言源代码转换为机器语言目标代码的过程。汇编语言是一种低级编程语言,它使用助记符来代表机器指令,这使得编程者可以更容易地编写和理解代码,而无需直接处理二进制机器码。

    4.1.2 汇编的作用
    • 转换:汇编器将汇编语言源代码中的指令转换为机器可以执行的二进制指令码。这是通过查找一个称为“汇编指令集”的表来完成的,该表将助记符映射到相应的机器指令码。
    • 符号解析:在汇编过程中,汇编器还会处理符号(如变量名、函数名等)。它将这些符号转换为它们在内存中的地址或偏移量。这些地址或偏移量随后会被嵌入到目标代码中,以便在运行时能够正确地访问它们。
    • 生成目标文件:一旦汇编过程完成,汇编器就会生成一个目标文件(如 hello.o)。这个目标文件包含了机器指令码以及符号表和其他元数据,这些信息在后续的链接过程中会被使用。
    • 错误检查:汇编器还会检查源代码中的错误,如语法错误、无效的指令或未定义的符号等。如果发现错误,汇编器会生成一个错误消息,并停止汇编过程。

    4.2 在Ubuntu下汇编的命令

    图4.1 将hello.s汇编为hello.o的命令

    4.3 可重定位目标elf格式

    图4.2 输出并打开ELF文件的命令

    为了方便,我们可以直接用VS打开所生成的hello.elf文件,下文皆如此。

    4.3.1 ELF Header

    在 ELF文件中,ELF Header 是文件的第一个部分,它包含了关于整个文件的基本信息。对于此hello.o文件,以下是本ELF Header 包含的主要信息:

    • Magic:***********(文件的类型)
    • Class:ELF64位(ELF 文件的类)
    • Data: 补码存储,小端存储(数据存储方式,字节序类别)
    • Version:**********(ELF文件的版本)
    • OS/ABI:标识操作系统和 ABI(应用程序二进制接口)
    • ABI Version:标识 ABI 的版本
    • Type:可重定位文件(ELF文件的类型)
    • Machine:AMD_X86_64(目标机器的类型)
    • Version:ELF头的版本(通常为 1)。
    • Entry Point Address:对于可执行文件或共享库,这是程序开始执行的地址。对于对象文件,这个字段通常是未定义的(通常为 0)。
    • Start of Program Headers:程序头表的偏移量。对于对象文件,这个字段通常是 0,因为对象文件不包含程序头表。
    • Start of Section Headers:节头表的偏移量。节头表描述了文件中各个节(如代码、数据、符号表等)的信息。
    • Flags:与处理器相关的标志。
    • Size of this header:ELF的大小(以字节为单位)。

    图4.3 ELF Header部分信息

    4.3.2 Section Headers

    此hello.o文件中,Section Headers提供了关于文件中各个节(Sections)的信息。每个节都包含特定类型的数据,如代码、数据、符号表等。以下是本Section Headers包含的信息:

    • 节名(Name):指定节的名称,如.text、.data等。
    • 节类型(Type):描述节的内容类型,如SHT_PROGBITS、SHT_SYMTAB等。
    • 虚拟地址(Address):可执行文件和共享库中节的内存起始地址。
    • 文件偏移(Offset):节在ELF文件中的起始字节偏移量。
    • 节大小(Size):节的长度(字节)。
    • 节条目大小(Entry Size):固定大小条目的大小(字节),无则为0。
    • 节标志(Flags):描述节的属性,如是否可执行、可写、可分配内存等。
    • 链接索引(Link):指向与当前节相关的其他节(如字符串表)。
    • 信息索引(Info):提供有关节的额外信息。
    • 对齐要求(Align):节在内存或文件中的对齐要求。

    图4.4 Section Headers部分信息

    此hello.o文件中,Key to Flags用于解释在Section Headers中Flag字段的每一位所代表的含义。具体如下:

    图4.5 Section Headers部分剩余信息

    4.3.3 Relocation section

    此hello.o文件中,Relocation section包含了关于如何修改文件中的代码和数据引用的信息,以便在链接时与其他目标文件或共享库中的符号进行正确的地址解析。以下是本Relocation section包含的信息:

    • Offset:表示在目标文件中需要修改的字段的偏移量。
    • Info:这是一个包含符号索引和类型信息的字段。
    • Type:表示重定位的类型,它决定了如何计算新的地址。例如,R_X86_64_PC32表示基于程序计数器(PC)的相对地址重定位,而R_X86_64_64可能表示直接的64位绝对地址重定位。
    • Sym. Value:指符号(Symbol)的值。在链接过程中,符号通常代表一个函数、变量或其他在目标文件中定义的实体
    • Sym. Name:用于在目标文件中唯一标识一个实体(如函数或变量)。在链接过程中,链接器会根据符号的名称来匹配符号的定义和引用。
    • Addend:对于某些重定位类型,可能需要一个额外的值(Addend)来完成地址计算。这个值通常直接包含在重定位条目中。

    图4.6 Relocation section部分信息

    4.3.4 Symbol table

    此hello.o文件中,Symbol table(符号表)包含了关于程序中定义和引用的函数、变量等符号的信息。符号表对于链接器(linker)和调试器(debugger)等工具来说是非常重要的,因为它们需要这些信息来解析符号引用和进行调试。以下是本Symbol table中包含的信息:

    • Num:从0开始按出现顺序排序的符号。
    • Value:符号值。对于已定义符号,是地址或偏移量;对于未定义符号,可能是0或特殊标记值。
    • Size:符号大小。表示符号在内存中的字节数,取决于元素或成员的大小。
    • Type:符号类型。如函数、变量等,对链接器很重要,决定如何处理符号引用。
    • Bind:符号绑定。表示符号的可见性和链接属性,如局部或全局。
    • Vis:符号可见性。更具体地表示符号在链接过程中的可见性,如默认或隐藏。
    • Ndx:节头索引。指示符号所属的目标文件节,用于关联符号与实际位置。
    • Name:符号名称。符号的唯一标识符,如函数名或变量名。

    图4.7 Symbol table部分信息

    4.4 Hello.o的结果解析

    本节中,我们对hello.o文件进行反汇编并输出hello.asm文件,并对hello.asm与hello.s文件内容进行对比分析,具体命令如下图:

    图4.8 反汇编并输出hello.asm的命令

    4.4.1 反汇编代码
    1. hello.o:     file format elf64-x86-64

    1. Disassembly of section .text:

    1. 0000000000000000
      :
    2. 0: f3 0f 1e fa           endbr64
    3. 4: 55                    push   %rbp
    4. 5: 48 89 e5              mov    %rsp,%rbp
    5. 8: 48 83 ec 20           sub    $0x20,%rsp
    6. c: 89 7d ec              mov    %edi,-0x14(%rbp)
    7. f: 48 89 75 e0           mov    %rsi,-0x20(%rbp)
    8. 13: 83 7d ec 05           cmpl   $0x5,-0x14(%rbp)
    9. 17: 74 16                 je     2f
    10. 19: 48 8d 3d 00 00 00 00 lea    0x0(%rip),%rdi        # 20
    11. 1c: R_X86_64_PC32 .rodata-0x4
    12. 20: e8 00 00 00 00        callq  25
    13. 21: R_X86_64_PLT32 puts-0x4
    14. 25: bf 01 00 00 00        mov    $0x1,%edi
    15. 2a: e8 00 00 00 00        callq  2f
    16. 2b: R_X86_64_PLT32 exit-0x4
    17. 2f: c7 45 fc 00 00 00 00 movl   $0x0,-0x4(%rbp)
    18. 36: eb 53                 jmp    8b
    19. 38: 48 8b 45 e0           mov    -0x20(%rbp),%rax
    20. 3c: 48 83 c0 18           add    $0x18,%rax
    21. 40: 48 8b 08              mov    (%rax),%rcx
    22. 43: 48 8b 45 e0           mov    -0x20(%rbp),%rax
    23. 47: 48 83 c0 10           add    $0x10,%rax
    24. 4b: 48 8b 10              mov    (%rax),%rdx
    25. 4e: 48 8b 45 e0           mov    -0x20(%rbp),%rax
    26. 52: 48 83 c0 08           add    $0x8,%rax
    27. 56: 48 8b 00              mov    (%rax),%rax
    28. 59: 48 89 c6              mov    %rax,%rsi
    29. 5c: 48 8d 3d 00 00 00 00 lea    0x0(%rip),%rdi        # 63
    30. 5f: R_X86_64_PC32 .rodata+0x2c
    31. 63: b8 00 00 00 00        mov    $0x0,%eax
    32. 68: e8 00 00 00 00        callq  6d
    33. 69: R_X86_64_PLT32 printf-0x4
    34. 6d: 48 8b 45 e0           mov    -0x20(%rbp),%rax
    35. 71: 48 83 c0 20           add    $0x20,%rax
    36. 75: 48 8b 00              mov    (%rax),%rax
    37. 78: 48 89 c7              mov    %rax,%rdi
    38. 7b: e8 00 00 00 00        callq  80
    39. 7c: R_X86_64_PLT32 atoi-0x4
    40. 80: 89 c7                 mov    %eax,%edi
    41. 82: e8 00 00 00 00        callq  87
    42. 83: R_X86_64_PLT32 sleep-0x4
    43. 87: 83 45 fc 01           addl   $0x1,-0x4(%rbp)
    44. 8b: 83 7d fc 09           cmpl   $0x9,-0x4(%rbp)
    45. 8f: 7e a7                 jle    38
    46. 91: e8 00 00 00 00        callq  96
    47. 92: R_X86_64_PLT32 getchar-0x4
    48. 96: b8 00 00 00 00        mov    $0x0,%eax
    49. 9b: c9                    leaveq
    50. 9c: c3                    retq   
    4.4.2 比较分析

    将hello.asm和hello.s进行比较,大部分相同,主要有一下几个方面不同:

    • 包含内容:hello.s包含.type、.size、.align及.rodata等信息,而hello.asm仅含函数内容。
    • 分支转移:hello.s中跳转指令目标地址记为段名称,如.L1、.L2等;hello.asm中跳转目标为具体地址。
    • 函数调用:hello.s中call后直接跟函数名;hello.asm中call目标为下一条指令地址。
    • 全局变量访问:hello.s使用段名称+%rip访问rodata;hello.asm使用0+%rip访问。
    4.4.3 机器语言与汇编语言的映射关系

    机器语言和汇编语言之间的映射关系主要体现在汇编语言指令与机器语言指令的对应关系。汇编语言使用人类可读的助记符表示机器指令,而机器语言是计算机硬件直接执行的二进制指令集。

    以此hello.asm为例:

    任意指令之前所对应的数字即为其相对应的机器代码。汇编器通过查找指令集映射表将汇编指令转换为机器指令编码,并写入二进制文件。需注意不同CPU架构和指令集具有不同的汇编和机器语言规范,因此映射关系也不同。

    4.5 本章小结

    本章我们讨论了汇编阶段将汇编代码hello.s翻译成机器语言指令hello,o的过程,并对汇编文件的产生进行了详细论述。同时,我们比较了汇编代码与反汇编代码之间的不同之处。这对于理解汇编语言的构成于生成机制大有裨益。本章所得到的hello.o文件将作为下一章节链接器(ld)的输入文本。


    5链接

    5.1 链接的概念与作用

    5.1.1 链接的概念

    链接是将多个编译单元(通常是目标文件)合并成一个可执行文件或库文件的过程。在这个过程中,链接器(Linker)会解析符号引用,将定义在其他文件中的符号与在当前文件中使用的符号进行匹配,并生成最终的二进制文件。

    5.1.2 链接的作用
    • 符号解析:在编译过程中,编译器为每个全局变量、函数等分配唯一标识符。链接器负责确定这些符号在内存中的实际地址,并解析目标文件中的符号引用,与定义进行匹配。
    • 重定位:链接器对目标文件中的代码和数据进行重定位。编译器为每个目标文件生成相对地址表,链接器根据符号实际地址更新引用位置,确保符号引用正确指向目标。
    • 合并目标文件:链接器将所有目标文件合并成可执行文件,处理依赖关系,确保必要库和其他目标文件被包含。按目标平台规范组织可执行文件,设置入口点、初始化段等。
    • 优化和压缩:一些链接器提供优化和压缩功能,如删除未使用函数和数据段、内联代码、死代码消除等,以减少可执行文件大小并提高执行效率。

    5.2 在Ubuntu下链接的命令

    图5.1 链接多个共享库生成hello的命令

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

    此步我们可以将hello文件中ELF输出为单独文件,便于分析,命令如下:

    图5.2 输出hello文件ELF部分的命令

    而后我们可以用VS直接打开hello.elf2文件进行分析。

    !!!声明!!!:此节内容多与4.3节内容相同,包括ELF Header、Section Headers、Relocation section与Symbol table部分,下文不再赘述。

    对于不同部分的分析如下:

    5.3.1 Program Headers

    在hello.elf2文件中,每个 Program Header 都是一个 Elf64结构体的实例,它包含了以下字段:

    • Type:段的类型。例如,LOAD 表示这个段应该被加载到内存中,DYNAMIC 表示这个段包含了动态链接信息。
    • Offset:段在文件中的偏移量。
    • VirtAddr:段的虚拟地址(当这个段被加载到内存中时)。
    • PhysAddr:段的物理地址(这个字段在现代系统中很少使用,通常被设置为 0)。
    • FileSiz:段在文件中的大小。
    • MemSiz:段在内存中的大小。这通常大于或等于FileSiz,因为某些段(如 BSS 段)在文件中没有数据,但在内存中需要分配空间。
    • Flags:段的标志。例如,X 表示这个段是可执行的,W 表示这个段是可写的,R 表示这个段是可读的。
    • Align:段在内存和文件中的对齐要求。

    图5.3 Program Headers部分信息

    5.3.2 Dynamic section

    在hello.elf2文件中,Dynamic section包含了在运行时由动态链接器使用的信息。这些信息对于解析依赖项、初始化程序、以及处理动态链接过程中的其他任务至关重要。

    Dynamic section是由一系列的Elf64结构体组成的,每个结构体都包含一个标签(Tag)和一个相关的值(Value)。这些标签和值定义了动态链接器所需的各种属性和行为。一些常见的Dynamic section标签包括:

    • NEEDED:表示ELF文件依赖的共享库,动态链接器在运行时加载。
    • PLTRELSZ:表示过程链接表(PLT)的重定位表大小(字节为单位)。
    • PLTGOT:是全局偏移表(GOT)的地址,用于存储函数和变量的地址。
    • HASH:是符号哈希表的地址,用于加速符号查找。
    • STRTAB:是字符串表的地址,包含动态节中的字符串。
    • SYMTAB:是符号表的地址,包含ELF文件中符号的信息。
    • INIT:是初始化函数的地址,动态链接器加载库后调用。
    • FINI:是终止函数的地址,共享库不再需要时动态链接器调用。

    图5.4 Dynamic section部分信息

    5.4 hello的虚拟地址空间

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

    程序被载入至地址0x401000~0x402000中。在该地址范围内,每个节的地址都与前一节中节对应的 Address 相同。通过ELF可知,程序从0x00401000到0x00400fff,在0x400fff之后存放的是.dynamic到.shstrtab节的内容。

    图5.5 edb查看虚拟地址空间窗口

    5.5 链接的重定位过程分析

    本节中,我们对hello文件进行反汇编并输出hello.asm2文件,分析hello与hello.o的不同,并结合hello.o的重定位项目,阐述hello中重定位机制。具体命令如下图:

    图5.6 反汇编并输出hello.asm2的命令

    5.5.1 重定位概念

    在链接过程中,重定位指的是将程序中的符号引用转换为它们在内存中的实际地址的过程。根据符号引用的类型和上下文,重定位可以分为几种不同的类型:

    • 绝对重定位:直接将符号引用的地址替换为一个固定的绝对地址值。这种重定位类型较少见,通常只在某些特殊情况下使用。
    • 基于段的重定位:将符号引用的地址替换为一个基于某个段基址的偏移量。这是最常见的重定位类型之一,它允许链接器在分配地址空间时具有一定的灵活性。
    • 基于符号的重定位:将符号引用的地址替换为另一个符号的地址加上一个偏移量。这种重定位类型通常用于处理函数内的局部变量或数组元素等。

    5.5.2 hello与hello.o文件的不同

    hello.o与 hello在内容上有几个显著的不同点。以下是这些不同的主要方面:

    • 文件格式:hello.o文件是按照ELF格式组织的,这种格式包含了程序的不同部分(如代码段、数据段、符号表等),但还没有进行最终的地址分配和符号解析。hello文件也是按照ELF格式组织的,但它已经包含了程序在内存中的最终布局,所有的符号引用都已经解析为具体的内存地址。
    • 地址空间:hello.o文件中的代码和数据通常使用相对地址,这些地址在hello文件中会被替换为具体的物理地址或虚拟地址。
    • 符号信息:hello.o文件中通常包含未解析的符号引用(如外部函数或变量的引用),以及符号的定义(如函数或全局变量的定义)。这些符号信息用于链接器在链接过程中解析符号引用。hello文件中的符号引用已经被解析为具体的内存地址,通常不再包含未解析的符号引用。
    • 依赖关系:Hello.o文件可能依赖于其他目标文件或库文件,这些依赖关系在链接阶段被解析。hello文件通常不依赖于其他目标文件或库文件(除了动态链接库),它们包含了程序运行所需的所有代码和数据。

    5.5.3 hello中重定位机制

    • 地址重定位:目标文件和库文件使用相对地址表示代码和数据。链接器需将相对地址转换为绝对地址。而后链接器遍历符号表,转换地址并修改文件段。需确定每段在内存中的位置,并修改地址引用。
    • 符号重定位:当存在相同符号时,链接器需解决符号冲突,常用重定位表记录地址信息并重新分配符号,确保引用指向正确地址。
    • 重定位条目的处理:汇编器生成目标模块时,对未知位置的目标引用生成重定位条目,告知链接器如何修改引用。链接器读取重定位条目,修改地址引用,如指令中的地址值或数据段中的指针值。

    5.6 hello的执行流程

    本节我们使用gdb执行hello,设置断点后逐步执行。可以得出其调用与跳转的各个子程序名和程序地址如下表:

    表5. 1 hello执行全过程调用的函数名称及程序地址

    程序名称

    程序地址

    <_start>

    4010f0

    <__libc_csu_init>

    4011c0

    <_init>

    401000

    401125

    <.plt>

    401020

    401090

    4010a0

    4010c0

    4010d0

    4010e0

    4010b0

    <__libc_csu_fini>

    401230

    <_fini>

    401238

    5.7 Hello的动态链接分析

    GOT(全局偏移表)是存储共享库中全局变量和函数地址的全局数据结构。在运行时,GOT被初始化,各元素指向对应变量或函数地址。程序访问全局变量或函数时,先访问GOT地址,再跳转至实际地址。

    GOT实现共享库动态链接,支持进程共享库,动态加载卸载。同时支持高级特性,如延迟绑定、符号重定位。

    PLT(过程链接表)是动态链接器数据结构,用于运行时动态绑定函数调用。PLT通过跳转指令实现动态绑定,首次调用时保存真实地址至GOT。后续调用直接跳转至GOT中保存的函数地址。

    本节我们使用gdb执行hello,在init处设置断点可以区分动态库链接的前后阶段,而后分别查询0x404000(详见图5.4 143行)可以得出其调用与跳转的各个子程序名和程序地址,具体如下:

    图5.7 gdb 调试动态链接

    5.8 本章小结

    本章我们探讨了链接的概念和作用,分析可执行文件hello的ELF格式及其虚拟地址空间,并对重定位过程、动态链接进行了深入的分析。


    6hello进程管理

    6.1 进程的概念与作用

    6.1.1 进程的概念

    进程是操作系统进行资源分配和调度的基本单位,是操作系统结构的基础。它指的是一个程序在给定的工作空间和数据集上的一次运行活动。或者说,它是操作系统中执行的一个指令序列,是一条正在运行的程序。

    6.1.2 进程的作用

    • 资源分配和调度:操作系统分配资源给进程,用调度算法决定执行顺序。
    • 提高系统并发性:通过进程,操作系统可并发执行程序,提高吞吐量和资源利用率。
    • 实现系统虚拟化:通过分配唯一PID和独立地址空间,操作系统实现多进程并发运行,互不干扰。
    • 提供程序执行环境:进程为程序提供独立环境,包括计数器、寄存器和内存空间,确保程序在受保护环境中执行。
    • 实现系统交互:进程是操作系统与用户、应用程序间交互的桥梁,实现用户请求处理、应用启动和终止等功能。

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

    6.2.1 Shell-bash的作用
    • 命令解释器:bash解释并执行用户输入的命令行指令,包括简单命令和复杂脚本。
    • 任务自动化:bash脚本可自动化执行重复性任务,如文件处理、监控、备份等。
    • 系统交互:bash提供与操作系统交互的接口,用于管理资源、配置参数、运行程序等。
    • 管道和重定向:bash支持管道和重定向,实现命令间数据传递和输出保存。
    • 环境变量:bash允许设置和使用环境变量,影响shell和程序行为。
    • 别名和函数:bash支持设置命令别名和定义函数,简化操作。

    6.2.2 Shell-bash的处理流程

    • 读取输入:bash从标准输入或脚本文件读取命令。
    • 解析命令:bash解析命令,识别命令名、选项、参数等。
    • 查找命令:bash在PATH目录中查找命令对应的可执行文件。如找到,执行;否则输出错误。
    • 执行命令:内部命令直接执行;外部命令创建新进程执行。
    • 等待命令完成:bash等待外部命令执行完成并获取退出状态码,判断命令是否成功。
    • 输出结果:命令执行后,bash显示输出或重定向到文件。
    • 继续读取输入:bash等待新命令,重复处理流程。

    6.3 Hello的fork进程创建过程

    在Linux系统中,fork进程创建过程可概述如下:

    • 函数调用:父进程调用fork()函数创建子进程。
    • 内核处理:控制转移至内核fork代码。内核分配内存、拷贝数据结构,并将子进程加入系统进程列表。
    • 写时拷贝:父子进程共享只读物理内存页。当进程尝试修改时,内核创建内存副本,允许写操作,不影响其他进程。
    • 返回值:fork()在父进程返回子进程PID,在子进程返回0。失败则返回-1并设置errno。

    注意:父子进程拥有各自地址空间、文件描述符表和环境变量副本。子进程修改不影响父进程。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。

    图6.1 程序不断fork的过程

    6.4 Hello的execve过程

    execve是Linux操作系统中负责执行可执行文件的核心系统调用。在调用execve时,需传入文件路径、命令行参数以及环境变量等三个关键参数。一旦调用成功,它将全面替换当前进程的代码段为指定文件的执行内容,并依据全新的参数和变量配置来启动新程序的执行。

    在实际应用中,通常会先通过fork系统调用创建一个子进程,随后在子进程中发起execve调用。execve的主要职责在于将目标程序加载至进程的地址空间,并精确设置程序计数器指向新程序的入口点。同时,它还会负责将命令行参数及环境变量等信息传递给新程序。

    当加载器成功跳转到新程序的入口点时,即意味着进程的控制权已转移至新程序。新程序将从入口点开始执行,通常这个入口点对应于start地址。在start函数中,会进一步调用main函数,从而完成子进程中新程序的加载与初始化过程。

    6.5 Hello的进程执行

    6.5.1 进程上下文信息

    进程上下文信息是操作系统执行进程时记录的信息,包括:

    • 寄存器的值:记录进程执行过程中各寄存器的值,确保上下文切换后进程能继续执行。
    • 程序计数器(PC)的值:记录进程最后执行的指令地址,以便上下文切换后从正确位置继续。
    • 栈指针的值:记录进程栈指针的值,确保上下文切换后栈状态正确恢复。
    • 内存映像:记录进程在内存中的映像,确保上下文切换后访问正确的内存空间。
    • 进程状态信息:记录进程的状态,如运行状态、等待状态、挂起状态等,便于管理进程状态。
    6.5.2 进程时间片

    进程时间片(Process Time Slice)是操作系统中用于多任务处理(也称为时间共享或多任务并发)的一个核心概念。在多任务操作系统中,CPU时间被划分为多个片段,每个片段称为一个时间片(Time Slice),并分配给不同的进程使用。

    当一个进程获得CPU的使用权并开始执行时,它会被分配一个时间片。如果在这个时间片结束之前,进程没有完成它的任务或者因为某种原因(如等待I/O操作)而阻塞,那么CPU会停止执行该进程,并将CPU的使用权转交给其他等待执行的进程。当该进程再次变为可运行状态时(例如,I/O操作完成),操作系统会再次为它分配一个时间片,使其能够继续执行。

    6.5.3 进程调度

    进程调度是操作系统在特定时刻进行的一项重要决策,旨在确定哪个进程将获得CPU的执行权。这一过程涉及对正在运行的进程进行优先级排序和调度执行,是操作系统对系统资源公平、高效分配的关键环节。通过合理的进程调度,操作系统能够显著提升系统的吞吐量和响应时间,从而确保系统运行的稳定性和效率。

    6.5.4 用户模式和内核模式

    用户模式是操作系统中为用户级应用程序提供的执行环境。在此模式下,应用程序代码被限制在有限的资源访问权限和受限的操作系统服务中。用户模式有如下特点:

    1. 应用程序代码在此模式下运行。
    2. 无法直接访问硬件(除了一些受限制的I/O操作)。
    3. 无法访问或修改内核代码和数据结构。
    4. 访问系统资源(如内存、文件等)时,需要通过系统调用(system call)来请求内核的协助。
    5. 错误处理通常不会导致整个系统崩溃,只会影响运行中的用户进程。

    内核模式是操作系统内核(Kernel)执行的特权模式。它允许无限制地访问系统内存、硬件和所有系统资源。内核模式有如下特点:

    1. 操作系统内核代码在此模式下运行。
    2. 可以直接访问和修改所有硬件资源。
    3. 负责管理内存、进程、文件、设备等系统资源。
    4. 响应系统调用,为用户提供服务。
    5. 错误处理不当可能导致整个系统崩溃。

    用户模式和内核模式是操作系统中两种重要的执行模式,它们通过系统调用来进行交互,共同实现了操作系统的安全性和稳定性。

    6.6 hello的异常与信号处理

    异常可以分为四类:中断(interrupt),陷阱(trap),故障(fault)和终止(abort)。

    中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。在当前指令完成执行后,处理器注意到中断引脚的电压变高,就从系统总线读取异常号,然后调用适当的中断处理程序。当处理程序返回后,它就将控制返回给下一条指令。

    陷阱是有意的异常,是执行一条指令的结果。应用程序执行一次系统调用,然后把控制传递给处理程序,陷阱处理程序运行后,返回到syscall之后的指令。

    故障由错误情况引起,故障发生时处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。

    终止是不可恢复的致命错误造成的结果。终止处理程序从不将控制返回给应用程序,处理程序将控制返回给一个abort例程,该例程会终止这个应用程序。

    以下给出程序运行过程中多种异常与信号处理的结果图,如乱码输入(包括Enter)、Ctrl-C与Ctrl-Z后运行ps、jobs、kill、pstree、fg等命令的运行结果:

    图6.2 乱码输入结果图

    图6.3 输入Ctrl-C后的结果图

    图6.4 输入Ctrl-Z后运行ps、jobs与kill的结果图

    图6.5 输入Ctrl-Z后运行pstree的结果图

    图6.6 输入Ctrl-Z后运行fg的结果图

    6.7本章小结

    本章我们主要讨论了进程和shell的概念与作用,详细论述了进程的创建和执行过程,以及对多种异常和信号的处理有了直观的认识。


    7hello的存储管理

    7.1 hello的存储器地址空间

    • 逻辑地址:逻辑地址是程序源代码或编译后的目标代码中使用的地址,它表示数据或指令在程序中的相对位置。在C语言程序中,当我们声明一个字符串如char str[] = "hello";时,编译器会为这个字符串分配一个逻辑地址。这个地址是相对于程序的起始地址或某个段的起始地址的偏移量。逻辑地址是相对的,不是绝对的物理位置。在程序执行前,逻辑地址需要被转换为物理地址才能被CPU访问。
    • 线性地址:线性地址是逻辑地址到物理地址转换过程中的一个中间层。在分段机制中,逻辑地址(段中的偏移地址)加上段基址(段地址)就形成了线性地址。如果“hello”字符串在程序的某个段中,它的逻辑地址加上该段的基地址就会形成一个线性地址。例如,如果逻辑地址是0x1000,段基址是0x200000,那么线性地址就是0x201000。线性地址是一个连续的、一维的地址空间,用于简化内存管理。
    • 虚拟地址:虚拟地址是程序在运行时所使用的地址,特别是在使用虚拟内存管理(如分页机制)的操作系统中。虚拟地址空间为每个进程提供了一个独立的、受保护的地址范围。操作系统中,每个运行的程序(包括“hello”程序的进程)都有一个独立的虚拟地址空间。在这个空间中,“hello”字符串会有一个虚拟地址。这个地址在进程内部是唯一的,但可能与其他进程的地址不同。虚拟地址通过页表等机制映射到物理地址。这允许操作系统实现内存保护、地址空间隔离和动态内存分配等功能。
    • 物理地址:物理地址是内存中各存储单元的真实、唯一的编号。它是硬件可以直接访问的地址。在物理内存中,“hello”字符串的实际内容会被存储在一个或多个物理地址上。这些地址是固定的,并且可以通过直接内存访问(DMA)等方式被硬件访问。物理地址是内存中的实际位置,与逻辑地址、线性地址和虚拟地址不同。它们之间的关系需要通过地址转换机制(如分段、分页)来确定。

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

    线性地址是逻辑地址到物理地址转换过程中的一个中间层。在分段机制中,逻辑地址(段中的偏移地址)加上段基址(段地址)就形成了线性地址。如果“hello”字符串在程序的某个段中,它的逻辑地址加上该段的基地址就会形成一个线性地址。例如,如果逻辑地址是0x1000,段基址是0x200000,那么线性地址就是0x201000。线性地址是一个连续的、一维的地址空间,用于简化内存管理。

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

    页式管理是一种内存空间存储管理的技术,它将各进程的虚拟空间划分成若干个长度相等的页(page),同时把内存空间也按页的大小划分成片或页面(page frame)。页式管理通过建立页表来实现虚拟地址与物理地址之间的一一对应。

    线性地址由高位的“页目录索引”、中位的“页表索引”和低位的“页内偏移量”三部分组成。这三部分分别用于定位页目录、页表和页内的具体位置。

    从线性地址变换到物理地址的过程具体如下:

    • 查找页目录:使用线性地址的高位(页目录索引)作为索引,在页目录表+中查找对应的页目录项。页目录项中存储了页表的基地址(页表物理地址)
    • 查找页表:利用从页目录项中获得的页表基地址和线性地址的中位),在页表中查找对应的页表项(PTE)。页表项中存储了物理页面的基地址(物理页地址)和页面的访问权限。
    • 计算物理地址:将从页表项中获得的物理页面基地址与线性地址的低位(页内偏移量)相加,得到最终的物理地址。
    • 使用快表(TLB):为了提高地址转换的效率,现代处理器通常使用快表(TLB)来缓存最近使用的页目录项和页表项。当需要进行地址转换时,处理器首先会在TLB中查找,如果找到则直接返回物理地址,否则再进行上述的页目录和页表查找过程。

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

    TLB是硬件高速缓存,加速虚拟地址(VA)到物理地址(PA)的翻译。四级页表机制下,虚拟地址需四级映射得物理地址,访问内存降低性能。TLB缓存常用虚拟地址到物理地址映射,若CPU访问的虚拟地址在TLB中,则直接得物理地址,避免多次访问内存。

    四级页表机制中,虚拟地址32位,高10位页目录号,中10位页表号,低12位页内偏移量。CPU根据页目录号和页表号查页目录表和页表,得物理地址对应的页框号和偏移量,加页内偏移量得物理地址。若TLB无缓存对应映射,则经四级映射过程。若TLB有缓存映射,则直接从TLB得物理地址,此过程快于四级映射。

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

    在三级Cache支持下,CPU首先会在L1 Cache中查找数据,若未找到则继续在L2 Cache中查找,仍未找到则进一步在L3 Cache中查找。若三级Cache中均未找到,则从主存中获取并存储到L3 Cache中以加速下次访问。

    在物理内存访问时,CPU通过地址总线将地址发送到内存控制器。内存控制器解码地址,找到并读取所需物理内存数据,同时将其缓存到L3 Cache中以加速后续访问。

    7.6 hello进程fork时的内存映射

    在hello进程使用fork()系统调用创建子进程时,内存映射的过程可以概括为以下几个关键步骤:

    • 父进程内存映射:在fork()前,父进程已有完整内存映射,包括代码、数据、堆、栈及通过mmap()等映射的文件或设备。
    • 复制父进程的内存映射:fork()时,内核为子进程创建新描述符,复制父进程的内存映射。这是虚拟内存复制,采用共享和写时复制机制。
    • 写时复制:父进程和子进程共享内存映射,但采用写时复制。当任一进程修改内存页时,内核分配新物理页,写入修改,并更新页表。这样,两进程有各自物理页副本,互不影响。
    • 内存映射的继承:子进程继承父进程的内存映射布局,但物理内存上分离,直到发生写操作。
    • 地址空间隔离:尽管内存映射布局相似,但地址空间隔离。每个进程有自己页表,实现虚拟地址到物理地址的转换。进程间不能直接访问对方内存,除非使用特殊机制。
    • 页表与TLB:每个进程有页表,用于地址转换。现代处理器使用TLB缓存页表项,提高效率。
    • 内存映射的终止:进程终止时,内核回收相关内存映射和物理页。对于写时复制页面,若只有一个进程引用,则只回收一个物理页。

    7.7 hello进程execve时的内存映射

    当hello进程执行execve系统调用时,内存映射会经历一系列的变化。以下是对execve时内存映射变化的具体描述:

    • 删除已存在的用户区域:execve调用开始时,系统会删除当前进程中的用户空间内存映射,包括所有内存区域。
    • 映射新的程序文件:系统读取新程序文件内容,并映射到新的内存区域中,包括代码段、数据段等。
    • 创建新的页表:为进程创建新页表,将虚拟地址空间映射到物理内存或交换空间中的页。
    • 设置程序计数器:系统设置进程的程序计数器为新的程序入口点,即main函数的地址。
    • 内存映射的更新:hello进程的内存映射被新程序文件取代,页表更新以反映新的映射关系。
    • 地址空间的隔离:尽管execve调用会改变内存映射,但每个进程拥有独立地址空间,不能直接访问对方内存。

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

    hello进程在执行过程中可能会遇到缺页故障和缺页中断。以下是关于缺页故障与缺页中断处理的机制阐述:

    7.8.1 缺页故障

    当进程试图访问其地址空间中的一个页面时,该页面当前并不在物理内存中(即存在位为0),此时CPU会停止当前指令的执行,并通知操作系统内核产生了一个缺页故障。

    缺页故障的处理:处理器生成一个虚拟地址,并将它传送给内存管理单元(MMU)。MMU生成PTE地址,并从高速缓存/主存请求得到它。高速缓存/主存向MMU返回PTE。如果PTE中的有效位是0(表示页面不在内存中),则MMU触发一次异常,将CPU中的控制转移到操作系统内核中的缺页异常处理程序。缺页处理程序会确定需要从磁盘加载哪个页面,并将其加载到物理内存中。最后缺页处理程序返回到原来的进程,继续执行导致缺页的命令。

    7.8.2 缺页中断

    操作系统内核响应缺页故障时产生的一种特殊类型的中断。当CPU检测到缺页故障时,它会生成一个缺页中断,并将控制权转移到操作系统内核的缺页中断处理程序中。

    缺页中断的处理:缺页中断的处理过程与缺页故障的处理过程非常相似,因为两者都是由于页面不在物理内存中而导致的。在处理缺页中断时,操作系统内核会执行以下步骤:

    • 保存当前进程的上下文(如寄存器状态、指令指针等)。
    • 查找页面表,确定需要从磁盘加载哪个页面。
    • 如果该页面已被修改(即“脏”页面),则将其写回到磁盘上的交换空间。
    • 从磁盘加载所需的页面到物理内存中。
    • 更新页面表和其他相关数据结构,以反映新的物理页面映射。
    • 恢复之前保存的进程上下文,并继续执行导致缺页的命令。

    7.9动态存储分配管理

    在本hello进程中,动态内存管理的基本方法与策略主要包括以下几个方面:

    7.9.1 动态内存分配

    基本方法

    malloc/calloc/realloc :用于在堆上动态分配内存。malloc用于指定大小的内存块分配;calloc会额外将分配的内存初始化为零;realloc用于调整已分配内存块的大小。

    策略

    根据程序运行时的实际需求动态分配内存,避免静态分配导致的内存浪费或不足。使用malloc或new时,应检查返回值是否为NULL,以确保内存分配成功。

    7.9.2 动态内存释放

    基本方法

    free (C语言):用于释放之前通过malloc、calloc或realloc分配的内存。

    策略

    一旦不再需要动态分配的内存,应立即释放,以避免内存泄漏。释放内存后,应将指向该内存的指针置为NULL,防止野指针的出现。

    7.10本章小结

    本章节我们探讨了hello的存储管理。从hello的存储器地址空间起,我们逐步对线性地址到物理地址的翻译进行了详尽叙述。并且在内存映射的角度上回顾了fork和execve函数。而后又介绍了缺页故障和缺页中断处理机制,最后介绍了动态存储分配管理机制。


    8hello的IO管理

    8.1 Linux的IO设备管理方法

    Linux的IO设备管理方法主要基于Unix的IO接口,并在此基础上进行了扩展和优化。主要部分如下:

    • I/O管理子系统:Linux的I/O管理子系统涉及多种I/O操作机制和服务,包括缓冲与缓存、异步与同步I/O、I/O调度和内存映射I/O。
    • 缓冲与缓存机制:Linux内核通过缓存机制减少物理I/O操作次数,提高性能。
    • 异步I/O与同步I/O:Linux支持异步I/O操作,允许进程继续执行其他任务而不必等待I/O完成。同时支持同步I/O操作。
    • I/O调度:Linux内核使用I/O调度算法优化并发I/O请求的顺序和服务时间。
    • 内存映射I/O:通过mmap()系统调用,Linux实现设备内存到进程地址空间的映射,简化数据交换。
    • 设备驱动:设备驱动程序是内核与硬件的桥梁,负责硬件初始化、配置、提供接口、处理数据传输等。Linux的设备驱动模型为开发提供便利。
    • 设备模型:Linux引入设备模型,为设备和总线提供统一抽象结构,使设备管理和访问更高效。设备模型包括kobject、kset、bus_type等组件。

    8.2 简述Unix IO接口及其函数

    8.2.1 Unix IO模型

    Unix系统提供了多种I/O模型,包括:

    • 阻塞I/O:程序等待I/O操作完成后再继续执行。
    • 非阻塞I/O:程序在等待I/O时执行其他任务。
    • 多路复用I/O:程序同时监视多个I/O操作,提高效率和响应。
    • 异步I/O:程序发起I/O后继续执行其他任务,I/O完成后由操作系统通知。

    8.2.2 Unix I/O接口函数

    Unix I/O接口函数主要包括以下几个:

    • open:打开或创建一个文件,并返回一个文件描述符。int open(const char *pathname, int flags, mode_t mode);pathname为需要打开或创建的文件的路径名;flags为文件打开模式,如O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)等;mode为当文件被创建时,设置文件的访问权限。
    • close:关闭一个已打开的文件。int close(int fd);fd为需要关闭的文件的文件描述符。

    • read:从文件中读取数据。ssize_t read(int fd, void *buf, size_t count);fd为需要读取的文件的文件描述符;buf为用于存储读取数据的缓冲区;count为需要读取的字节数;
    • write:将数据写入文件。ssize_t write(int fd, const void *buf, size_t count);fd为需要写入的文件的文件描述符;buf为包含要写入数据的缓冲区;count为要写入的字节数。

    8.3 printf的实现分析

    printf函数的实现涉及多个步骤:从vsprintf生成显示信息、write系统函数再到系统调用。接着,字符显示驱动子程序将ASCII转换为字模库,并存储在vram中(包含每个点的RGB信息)。最后,显示芯片按刷新频率从vram读取数据,并通过信号线将每个点的RGB分量传输到液晶显示器。以下从这三部分进行阐述:

    8.3.1 阶段一

    从 vsprintf 生成显示信息、write 系统函数再到陷阱-系统调用

    • vsprintf 函数:核心格式化功能由 vsprintf完成。它接收格式化的字符串和参数,将结果写入用户提供的缓冲区。
    • write 系统函数:当 printf 需要发送信息到文件时,会调用 write 系统函数。此函数将数据从用户空间缓冲区发送到内核空间。
    • 系统调用:write 函数是包装器,最终会触发系统调用。在 Linux 中,这通常通过 syscall 指令完成(早期x86架构的Unix系统上,是通过 int 0x80 中断实现的)。系统调用是用户空间与内核空间之间的通信机制,允许用户空间程序请求内核服务。
    8.3.2 阶段二

    字符显示驱动子程序:ASCII到字模库到显示VRAM

    • ASCII到字模库:字符显示将ASCII字符转换为屏幕图像,通过查找字模库或字体完成。每个ASCII字符与字模库中一个字形相关联。
    • 字模库到显示VRAM:找到对应字形后,字形数据被转换并存储在VRAM中。VRAM存储屏幕图像,包含每个像素的RGB颜色信息。
    8.3.3 阶段三

    显示芯片按刷新频率从VRAM读取数据,通过信号线传输给液晶显示器,控制每个像素的亮度和颜色,显示完整图像。

    • 显示芯片:负责从VRAM读取图像数据,转换为显示器可显示的信号。
    • 刷新频率:显示器按固定频率更新屏幕内容,显示芯片逐行读取VRAM数据,生成驱动信号。
    • 信号线传输:显示芯片通过HDMI等信号线将信号传输到液晶显示器,信号包括RGB分量和其他信息。

    8.4 getchar的实现分析

    getchar 函数是 C 标准库中的一个函数,用于从标准输入(通常是键盘)读取一个字符。在 Unix 和 Linux 系统中,getchar 的实现通常与底层的键盘中断处理、系统调用以及用户空间与内核空间的交互密切相关,以下将从两个阶段分析getchar函数的实现:

    8.4.1 阶段一

    异步异常-键盘中断的处理

    • 中断触发:用户按键时,键盘控制器向 CPU 发送中断信号,表示键盘事件发生。
    • 中断服务程序:CPU 响应并跳转到 ISR 处理中断事件。
    • 读取扫描码:在 ISR 中,代码读取键盘控制器的扫描码,即每个键的唯一标识。
    • 转换扫描码到 ASCII:ISR 使用转换表将扫描码转为 ASCII 码。
    • 保存到键盘缓冲区:转换后的 ASCII 码保存在键盘缓冲区中,用于数据传递。
    8.4.2 阶段二

    getchar等调用read系统函数

    • 调用read:当getchar函数被调用时,它会使用底层的read系统函数从标准输入读取数据。
    • 系统调用:read函数是一个系统调用,它会导致从用户空间到内核空间的上下文切换。用户空间的参数被复制到内核栈上。
    • 内核处理:在内核中,read实现会检查键盘缓冲区是否有数据。如果有,它会从缓冲区取出数据并复制到用户空间。
    • 阻塞与非阻塞:如果键盘缓冲区无数据,read的行为取决于文件描述符是否设置非阻塞。若设置,read立即返回错误;否则,它等待数据。
    • 返回ASCII码:数据复制到用户空间后,read返回。getchar检查数据,若是有效ASCII码(非回车符),返回该码;若是回车符,等待下一个非回车符;若是错误或EOF,返回相应错误码。

    8.5本章小结

    本章节我们探讨了Linux I/O和Unix I/O的主要原理及组成部分,包括Linux的IO设备管理方法和Unix IO接口及其函数;而后分别对printf与getchar的实现进行了详尽分析。

    结论

    hello的生命周期,从预处理、编译、汇编到链接,最终生成可执行文件,并在内存中加载运行,直至内存回收,完成其使命。其详细过程包括:

    1. hello.c的创建

       程序员利用高级编程语言编写hello.c程序,并将其存储在内存中,作为整个生命周期的起点。

    2. 预处理阶段

       预处理器将hello.c中所有包含的外部头文件内容直接插入到程序文本中,并完成字符串的替换工作,为后续处理提供便利。

    3. 编译阶段

       编译器介入,将经过预处理的hello.i文件转化为汇编语言文件hello.s,为下一阶段做好准备。

    4. 汇编阶段

       汇编器将hello.s文件中的汇编指令翻译成机器语言指令,并将这些指令打包成可重定位目标程序格式,最终保存为hello.o目标文件。

    5. 链接阶段

       链接器负责将hello的程序编码与所需的动态链接库等资源整合在一起,生成一个单一的可执行目标文件hello。

    1. 加载与运行阶段

       在Shell环境中,通过键入./hello 2022113294 dc 13201587193 3命令启动程序。Shell为hello进程fork新建进程,并通过execve将代码和数据加载到虚拟内存空间。随后,程序开始执行。

    7. 指令执行阶段

       当该进程被操作系统调度时,CPU为其分配时间片。在一个时间片内,hello进程享有CPU的全部资源。程序计数器(PC寄存器)逐步更新,CPU不断取指并执行指令,按照控制逻辑流顺序执行。

    8. 内存访问阶段

       内存管理单元(MMU)负责将逻辑地址映射为物理地址。程序通过三级高速缓存系统访问物理内存或磁盘中的数据。

    9. 动态内存申请阶段

       在程序执行过程中,如printf等函数调用可能会通过malloc向动态内存分配器申请堆中的内存空间。

    10. 信号处理阶段

        hello进程在运行过程中时刻监听信号。若接收到如Ctrl+C或Ctrl+Z等信号,将调用相应的信号处理函数进行停止、挂起等操作。对于其他类型的信号,也有相应的处理机制。

    11. 终止与回收阶段

        当hello进程执行完毕或被终止时,Shell父进程负责等待并回收子进程资源。内核将删除为hello进程创建的所有数据结构,从而完成其生命周期的终结。


    附件

    表9. 1 论文撰写过程中生成的中间文件与功能备注

    文件名

    备注

    hello.c

    源代码文件

    hello.i

    预处理后的文本文件

    hello.s

    编译后的汇编文件

    hello.o

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

    hello

    链接后的可执行文件

    hello.elf

    hello.o生成ELF文件

    hello.asm

    反汇编hello.o的反汇编文件

    hello.elf2

    hello可执行文件生成的ELF文件

    hello.asm2

    反汇编hello得到的反汇编文件


    参考文献

    [1] Bryant, R. E., & O’Hallaron, D. R. (2016). Computer Systems: A Programmer’s Perspective (3rd ed.). Boston, MA: Pearson.

  • 相关阅读:
    阿里云对象存储OSS打造私人图床&私人云存储(1年仅9元)
    Netty(三)NIO-进阶
    【BOOST C++ 11 时钟数据】(3)时间(11-14)
    2010-2021年北大中国商业银行数字化转型指数数据(第三期)
    经典伴读_GOF设计模式_行为模式(下)
    小迪安全33WEB 攻防-通用漏洞&文件上传&中间件解析漏洞&编辑器安全
    榕树贷款使用PyTorch----激活函数
    基于JAVA的学校图书管理系统(Swing+GUI)
    Kafka 和 Spring整合Kafka
    Linux篇12文件系统inode和软硬链接
  • 原文地址:https://blog.csdn.net/2202_75648083/article/details/139397490