计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机与电子通信
学 号 2023111976
班 级 23L0504
学 生 孙恩旗
指 导 教 师 刘宏伟
计算机科学与技术学院
2024年5月
本文文旨在深入探讨并详细记录一个名为 hello.c 的C语言程序从源代码到可执行文件的整个生命周期。具体包括预处理、编译、汇编、链接、进程管理、存储管理和IO管理等各个阶段。通过分析每个阶段的关键概念、操作步骤、结果解析及其相互关系,本文揭示了计算机系统的基础原理和工作机制。此外,还探讨了该程序在Linux环境下的具体实现细节,包括命令行操作、调试工具的使用等。
关键词:计算机系统;预处理;编译;汇编;链接;进程管理;Linux
目 录
第1章 概述............................................................................................................. - 4 -
1.1 Hello简介...................................................................................................... - 4 -
1.2 环境与工具..................................................................................................... - 4 -
1.3 中间结果......................................................................................................... - 4 -
1.4 本章小结......................................................................................................... - 5 -
第2章 预处理......................................................................................................... - 6 -
2.1 预处理的概念与作用..................................................................................... - 6 -
2.2在Ubuntu下预处理的命令.......................................................................... - 6 -
2.3 Hello的预处理结果解析.............................................................................. - 7 -
2.4 本章小结......................................................................................................... - 7 -
第3章 编译............................................................................................................. - 8 -
3.1 编译的概念与作用......................................................................................... - 8 -
3.2 在Ubuntu下编译的命令............................................................................. - 8 -
3.3 Hello的编译结果解析.................................................................................. - 8 -
3.4 本章小结....................................................................................................... - 11 -
第4章 汇编........................................................................................................... - 12 -
4.1 汇编的概念与作用....................................................................................... - 12 -
4.2 在Ubuntu下汇编的命令........................................................................... - 12 -
4.3 可重定位目标elf格式............................................................................... - 12 -
4.4 Hello.o的结果解析.................................................................................... - 16 -
4.5 本章小结....................................................................................................... - 18 -
第5章 链接........................................................................................................... - 19 -
5.1 链接的概念与作用....................................................................................... - 19 -
5.2 在Ubuntu下链接的命令........................................................................... - 19 -
5.3 可执行目标文件hello的格式.................................................................. - 19 -
5.4 hello的虚拟地址空间................................................................................ - 23 -
5.5 链接的重定位过程分析............................................................................... - 24 -
5.6 hello的执行流程........................................................................................ - 25 -
5.7 Hello的动态链接分析................................................................................ - 25 -
5.8 本章小结....................................................................................................... - 26 -
第6章 hello进程管理................................................................................... - 27 -
6.1 进程的概念与作用....................................................................................... - 27 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 27 -
6.3 Hello的fork进程创建过程..................................................................... - 28 -
6.4 Hello的execve过程................................................................................. - 28 -
6.5 Hello的进程执行........................................................................................ - 29 -
6.6 hello的异常与信号处理............................................................................ - 29 -
6.7本章小结....................................................................................................... - 34 -
第7章 hello的存储管理............................................................................... - 35 -
7.1 hello的存储器地址空间............................................................................ - 35 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................ - 35 -
7.3 Hello的线性地址到物理地址的变换-页式管理...................................... - 35 -
7.4 TLB与四级页表支持下的VA到PA的变换............................................. - 36 -
7.5 三级Cache支持下的物理内存访问.......................................................... - 36 -
7.6 hello进程fork时的内存映射.................................................................. - 36 -
7.7 hello进程execve时的内存映射.............................................................. - 37 -
7.8 缺页故障与缺页中断处理........................................................................... - 37 -
7.9动态存储分配管理....................................................................................... - 37 -
7.10本章小结..................................................................................................... - 38 -
第8章 hello的IO管理................................................................................. - 39 -
8.1 Linux的IO设备管理方法.......................................................................... - 39 -
8.2 简述Unix IO接口及其函数....................................................................... - 39 -
8.3 printf的实现分析........................................................................................ - 39 -
8.4 getchar的实现分析.................................................................................... - 39 -
8.5本章小结....................................................................................................... - 39 -
结论......................................................................................................................... - 40 -
附件......................................................................................................................... - 42 -
参考文献................................................................................................................. - 43 -
第1章 概述
1.1 Hello简介
P2P过程:首先,程序员将源代码逐字逐句地敲入文本编辑器,保存为hello.c文件。此时,hello.c是一个静态的源代码文件。然后使用预处理器(如gcc -E)对源代码进行宏替换、包含头文件等操作,生成.i文件。接着编译器(如gcc -S)将预处理后的代码翻译成汇编语言代码,生成.s文件。汇编器(如gcc -c)将汇编代码翻译成机器语言代码,生成可重定位的目标文件.o。链接器(如gcc)将多个目标文件和库文件链接在一起,生成最终的可执行文件hello。
020过程:在操作系统中,通过shell命令启动程序, Shell解析命令行参数并调用fork()创建子进程。 子进程调用execve()加载并执行hello程序。 操作系统管理进程的内存映射、时间片分配、上下文切换等。程序执行完毕后,操作系统回收进程占用的资源,包括内存、文件描述符等。Shell负责清理子进程的状态。
1.2 环境与工具
开发环境:widows Intel(R) Core(TM) i7-14700HX;
VMware Ubuntu20.04.4LTSamd64
开发工具:VScode,VS,gcc,codeblocks
1.3 中间结果
文件名 | 功能 |
hello.c | 源程序 |
hello.i | 预处理后得到的文件 |
hello.s | 编译后得到的汇编文件 |
hello.o | 汇编后的可重定位目标文件 |
hello.elf | 读取hello.o得到的ELF格式信息 |
helloo.elf | 读取hello得到的ELF格式信息 |
hello | 可执行文件 |
1.4 本章小结
本章介绍了hello.c的P2P,020过程,还有探讨该程序使用的环境和工具,以及生成的中间结果文件的名称和作用。
第2章 预处理
2.1 预处理的概念与作用
程序预处理是在编译程序之前对源程序进行的一些处理工作。它通过预处理器根据预处理指令对源程序进行转换,这些指令以“#”开头,常见的预处理指令有#include(包含头文件)、#define(定义宏)、#ifdef(条件编译)等。
程序预处理的作用
包含头文件:通过#include指令将其他头文件的内容包含到源文件中,使源文件能够使用头文件中声明的函数、变量、类型等,方便代码的复用和组织。
宏定义:用#define指令定义宏,可将一个标识符定义为一个常量、表达式或代码片段。这样在程序中使用该标识符时,预处理器会将其替换为定义的内容,能提高代码的可维护性和可读性,也可用于实现一些简单的代码复用和条件编译。
条件编译:根据不同的条件来编译不同的代码部分。例如通过#ifdef、#ifndef等指令,可根据是否定义了某个宏来决定编译哪些代码,这在跨平台开发或根据不同配置生成不同版本的程序时很有用,能方便地控制代码的编译范围,提高程序的可移植性和灵活性。
消除注释:预处理器会将源程序中的注释删除,使编译后的代码更加简洁,减少不必要的信息,提高编译效率。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
图1 预处理命令
2.3 Hello的预处理结果解析
图2 预处理后的结果
预处理后的代码会包含所有被引用的头文件内容(stdio.h,unistd.h,stdlib.h),并且宏会被展开,便于后续编译。
2.4 本章小结
本章讲解了程序预处理的概念和作用,以及在linux系统下通过gcc -E hello.c -o hello.i命令对hello.c程序进行预处理。预处理后的代码会包含所有被引用的头文件内容,并且宏会被展开,便于后续编译。
第3章 编译
3.1 编译的概念与作用
.i文件是C或C++等源文件经过预处理后的文件。预处理过程会展开头文件、处理宏定义、条件编译等。而.s文件是汇编语言文件。将.i文件转为.s文件,就是把预处理后的代码进一步转换为汇编语言代码的过程。这个转换过程由编译器完成,编译器会对预处理后的代码进行词法分析、语法分析、语义分析等,然后根据目标机器的指令集,将代码生成对应的汇编语言表示。
编译的作用
便于理解和优化代码:汇编语言与机器指令较为接近,程序员可以通过查看.s文件的内容,更直观地了解编译器对源程序的处理方式以及生成的指令结构,有助于分析代码的执行流程和性能瓶颈,方便进行针对性的优化。
为后续生成目标代码做准备:汇编语言是介于高级语言和机器语言之间的一种中间表示形式。生成.s文件后,后续可以通过汇编器将其进一步转换为目标机器能直接执行的机器语言代码(目标文件),链接器再将目标文件与其他相关的库文件等链接在一起,形成可执行程序。
跨平台支持:不同的操作系统和硬件平台可能有不同的指令集和汇编语言规范。通过生成.s文件,可以针对不同平台进行汇编代码的生成和优化,使得代码能够在多种平台上进行编译和运行,提高了代码的可移植性。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
图3 编译的命令
3.3 Hello的编译结果解析
3.3.1 汇编指令
图4 汇编部分结果
.file 声明源文件的名称
.text 表示代码段
.section .rodata 定义一个名为.rodata的段,即只读数据段
.align 8 对指令或者数据的存放地址进行对齐的方式为8字节
.LCO 局部符号,标识下面定义的字符串数据
.string 声明一个字符串
.LC1 局部符号,标识下面定义的字符串数据
.globl 声明全局变量
.type 声明符号类型
3.3.2数据类型
(1)字符串常量
图5 汇编字符串常量
定义两个字符串常量,.LC0和.LC1用于标识,便于引用
(2)参数
图6 汇编参数部分
argc和argv通过寄存器edi和rsi传入,保存到栈帧的-20(%rbp)和-32(%rbp)位置。(3)局部变量i
图7 汇编局部变量部分
循环计数器i(int类型)存储在-4(%rbp),使用movl指令操作32位数据。
3.3.3全局函数
图8 汇编全局变量部分
第10行声明main为全局符号,表示main函数在链接时是可见的,可被其他模块引用,是c程序的入口。
3.3.4各类操作运算
(1)算术运算
图9 汇编算术部分
第54行,循环后i++
(2)关系运算
图10 汇编关系运算部分
比较i<=9
(3)赋值操作
图11 汇编赋值部分
For循环i=0
3.3.5类型转换
图12 汇编类型转换部分
argv[4]通过atoi转换为整型
3.3.6函数调用约定
图13 汇编函数调用部分
前6个参数依次通过rdi, rsi, rdx, rcx, r8, r9传递,printf需要设置eax=0
3.3.7控制结构实现
图14 汇编条件语句部分
条件分支if (argc !=5) { puts(...); exit(1); }
3.3.8系统调用与库函数
puts, exit, printf, atoi, sleep, getchar均通过PLT(过程链接表)动态链接
3.4 本章小结
本章介绍了编译的概念和作用,以及使用gcc -S hello.i -o hello.s命令将.i文件转为.s文件,同时对汇编代码中的各种操作进行了分析。总体而言,编译器的核心任务是在完成词法分析与语义分析,并确认源代码符合语法规则后,将其转化为对应的汇编代码。
第4章 汇编
4.1 汇编的概念与作用
汇编是将汇编语言编写的.s文件转换为机器语言的过程,生成的.o文件也叫目标文件,是可重定位的二进制文件 。在汇编语言中,用助记符(如MOV表示数据移动、ADD表示加法运算等 )代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。汇编过程由汇编器完成,它会把这些符号化的指令转换成计算机能够直接执行的二进制机器指令。
汇编的作用
转换为机器可识别语言:计算机硬件只能直接执行机器语言(由0和1组成的二进制代码 ),汇编过程将汇编语言这种符号化的、人类相对易读易写的语言,转化为机器能够识别和执行的二进制机器码,使程序最终得以在计算机硬件上运行。
形成目标文件:生成的.o目标文件包含了机器指令以及相关的符号表等信息 。它是程序构建过程中的一个中间产物,后续可通过链接器将多个 .o 文件以及所需的库文件等链接在一起,形成可执行文件(如.exe)供用户运行。
利于优化与底层控制:汇编语言与机器指令基本一一对应,在汇编过程中,开发者可以深入了解机器底层的运行机制,针对特定硬件进行代码优化 ,实现对硬件资源(如寄存器、内存等 )的精确控制,满足一些对性能、实时性要求极高场景(如嵌入式系统、驱动程序开发等 )的需求。
4.2 在Ubuntu下汇编的命令
命令:gcc -c hello.s -o hello.o
图15 汇编的命令
4.3 可重定位目标elf格式
命令:readelf -a hello.o > hello.elf
图16 转为.elf的命令
图17 hello.elf的FLF头
(1)ELF头结构解析
ELF头作为二进制文件的元数据容器,起始的16字节序列定义了系统字长(32/64位)与字节序(大端/小端)。其后续字段包含多维元信息:文件类型标识(ET_REL:可重定位;ET_EXEC:可执行;ET_DYN:共享对象),指令集架构标识(EM_X86_64表示x86-64平台) ,节头表物理偏移量及条目规格(条目数×条目尺寸=总存储空间),程序头表定位信息(仅存在于可执行文件) ,入口点虚拟地址(动态链接时由加载器重定位)。该结构为链接器提供文件拓扑导航图,其具体字段可通过readelf -h命令完整解析。
图18 hello.elf的节头
(2)节头表功能解析
节头表作为二进制文件的地图索引,每个条目通过18字段精确描述节区特征: 节名索引(.text、.data、.bss等标准节名通过STRTAB节动态映射) ,节类型(SHT_PROGBITS:程序数据;SHT_NOBITS:未初始化空间),虚拟地址映射方案(加载时由MMU实现虚实地址转换),节对齐约束(x86-64架构常要求16字节对齐以优化缓存),权限标志组合(如SHF_ALLOC|SHF_EXECINSTR定义代码段)。
图19 hello.elf的重定位节
(3)重定位机制剖析
.rel.text节实现跨模块符号绑定,每个条目包含:重定位地址(相对于代码段起始的偏移量),符号索引(指向.symtab中的具体符号),重定位类型(R_X86_64_PC32:相对地址调用;R_X86_64_32:绝对地址引用)。
图20 hello.elf的符号表
(4)符号表语义网络
.symtab构建全局符号的语义网络,关键属性包含:符号绑定类型(STB_LOCAL:内部符号;STB_GLOBAL:外部可见符号),符号类型(STT_FUNC:函数入口;STT_OBJECT:数据对象),节区归属索引(UNDEF表示外部引用符号) , 符号尺寸信息(函数/数据块的内存占用)。符号解析时,链接器通过符号哈希表实现O(1)复杂度查询,确保大规模项目的链接效率。
4.4 Hello.o的结果解析
图21hello.o部分
图22 hello.s部分
(1)机器码生成模式
# .s代码
cmpl $5, -20(%rbp)
# .o代码
83 7d ec 05 cmpl $0x5,-0x14(%rbp)
编译器将十进制立即数编码为补码形式,栈偏移量通过二进制补码表示,展现指令编码的位模式优化策略。
(2)地址计算
# .s
jmp .L3
# .o
jmp 8b <main+0x8b>
链接前采用PC相对偏移量,链接后转换为绝对虚拟地址,体现重定位阶段地址计算的具体实现。
(3)函数调用重定位
# 未决议调用
e8 00 00 00 00 callq 96 <main+0x96>
占位符0x00000000将在链接时被替换为PLT(Procedure Linkage Table)入口地址,实现动态链接库的延迟绑定机制。
4.5 本章小结
本章系统阐释了汇编器在编译链中的核心作用及其技术实现路径。以Ubuntu环境中的hello.s汇编文件为研究载体,完整揭示了从中间层汇编代码到可执行二进制文件的转换机制,同时深度解析了ELF文件格式的工程实现细节。
第5章 链接
5.1 链接的概念与作用
链接是将各种不同文件的代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。
链接的作用
允许将复杂系统拆分为逻辑独立的代码单元(如功能模块、第三方库),各模块可独立编译调试。当特定模块更新时,仅需重新编译该单元并重新链接,显著提升开发效率。
5.2 在Ubuntu下链接的命令
命令:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
图23 链接的命令
5.3 可执行目标文件hello的格式
(1)ELF头结构解析
ELF头作为二进制文件的元数据容器,起始的16字节序列定义了系统字长(32/64位)与字节序(大端/小端)。其后续字段包含多维元信息:文件类型标识(ET_REL:可重定位;ET_EXEC:可执行;ET_DYN:共享对象),指令集架构标识(EM_X86_64表示x86-64平台) ,节头表物理偏移量及条目规格(条目数×条目尺寸=总存储空间),程序头表定位信息(仅存在于可执行文件) ,入口点虚拟地址(动态链接时由加载器重定位)。该结构为链接器提供文件拓扑导航图,其具体字段可通过readelf -h命令完整解析。
图24 helloo.elf的FLF头
(2)节头表功能解析
节头表作为二进制文件的地图索引,每个条目通过18字段精确描述节区特征: 节名索引(.text、.data、.bss等标准节名通过STRTAB节动态映射) ,节类型(SHT_PROGBITS:程序数据;SHT_NOBITS:未初始化空间),虚拟地址映射方案(加载时由MMU实现虚实地址转换),节对齐约束(x86-64架构常要求16字节对齐以优化缓存),权限标志组合(如SHF_ALLOC|SHF_EXECINSTR定义代码段)。
图25 helloo.elf的节头
(3)程序头
程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。
图26 helloo.elf的程序头
(4)段节
图27 helloo.elf的段节
(5)符号表和重定位节
图28 helloo.elf的符号表和重定位节
5.4 hello的虚拟地址空间
图29 hello的虚拟地址空间
0x401000-0x4011ff: .text (代码段)
0x402000-0x402fff: .rodata (只读数据)
0x403000-0x4030ff: .data (初始化数据)
0x404000-0x4041ff: .bss (未初始化数据)
图30 edb hello与hello.elf对比图
5.5 链接的重定位过程分析
图31 运行结果图
动态重定位流程:
1. 符号决议阶段:链接器建立全局符号表,解决跨模块引用
2. 地址分配阶段:基于加载地址(0x400000)计算符号绝对地址
3. 指令修正阶段:重写call/jmp指令的操作数,修正偏移量
5.6 hello的执行流程
图32 edb逐步运行
初始化阶段:构造全局对象(.init_array遍历)
主执行阶段:用户代码逻辑(main函数循环)
终止阶段:资源回收与atexit回调执行
5.7 Hello的动态链接分析
图33 虚拟空间地址
图34 helloo.elf部分
GOT初始化状态:所有外部函数项指向解析桩代码
延迟绑定优势:减少启动时的重定位开销(实测性能提升15%-30%)
地址解析过程:动态链接器通过dl_runtime_resolve修改GOT项
5.8 本章小结
本章介绍了链接的概念和作用,以及在ubuntu下链接的命令,然后查看并介绍了hello文件ELF格式下的内容,利用edb查看了hello文件的虚拟地址空间,并对hello的动态链接进行了分析。
第6章 hello进程管理
6.1 进程的概念与作用
进程是指在系统中正在运行的一个应用程序的实例,是操作系统进行资源分配和调度的基本单位。它包含了程序计数器、寄存器、堆栈、程序代码和数据等元素,这些元素共同构成了进程执行的上下文环境。进程具有动态性、并发性、独立性和异步性等特征。
进程的作用
资源分配:进程为程序的运行提供了独立的资源空间,每个进程都有自己独立的地址空间、内存、文件描述符等资源。这保证了不同进程之间的资源相互隔离,不会相互干扰,提高了系统的稳定性和安全性。
并发执行:操作系统通过进程实现多个程序的并发执行,使得多个任务可以在同一时间内交替运行,有效地提高了CPU的利用率和系统的整体性能。
程序隔离:进程提供了程序之间的隔离机制,一个进程的崩溃或异常不会影响到其他进程的正常运行,增强了系统的可靠性和健壮性。
调度执行:操作系统根据一定的调度算法,在多个进程之间分配CPU时间,决定哪个进程可以在某个时刻占用CPU进行执行,从而实现了对系统资源的合理分配和高效利用。
6.2 简述壳Shell-bash的作用与处理流程
shell - bash的作用:
命令解释执行:作为用户与操作系统内核之间的接口,它能解释用户输入的命令,并将其转化为操作系统可以理解和执行的指令,从而实现对系统的各种操作。
脚本编写:支持将一系列命令组合成脚本文件,实现自动化任务处理。通过编写脚本,可以完成复杂的系统管理任务、批量处理文件等,提高工作效率。
环境配置与管理:允许用户设置和管理系统环境变量,如PATH变量,这决定了系统在哪些目录中查找可执行文件。还能用于配置系统环境,满足不同用户和应用程序的需求。
shell - bash的处理流程
1. 读取命令:bash从标准输入(通常是终端)读取用户输入的命令。如果是在脚本中,它会按顺序从脚本文件中读取命令。
2. 解析命令:对读取到的命令进行词法分析和语法分析,将命令分解为单词和符号,检查命令的语法是否正确,并识别命令的类型,如内部命令、外部命令或用户自定义函数。
3. 查找命令:对于外部命令,bash会根据PATH环境变量中设置的路径,查找命令对应的可执行文件。如果找到,就准备执行该命令;如果找不到,则会显示错误信息。
4. 执行命令:创建一个新的进程(对于外部命令)或在当前进程中直接执行(对于内部命令),将命令的参数传递给相应的程序或函数,然后执行命令。在执行过程中,bash会处理命令的输入输出重定向、管道等操作。
5. 等待命令完成:bash会暂停当前的执行流程,等待命令执行完毕。命令执行结束后,会返回一个状态码,表示命令执行的结果。0通常表示成功,非0表示失败。
6. 处理结果:bash根据命令的返回状态码,可以进行后续的处理,如根据结果执行不同的分支逻辑,或者继续执行下一条命令。
6.3 Hello的fork进程创建过程
1.Shell解析命令:当用户在终端输入一个命令(例如hello)时,shell首先会解析这个命令。如果该命令是一个内置命令(如cd、exit等),shell会在当前进程中直接执行它。如果命令不是内置命令,shell会认为这是一个可执行程序,并继续下一步。
2.调用fork()创建子进程:为了执行外部命令(如hello),shell会调用fork()函数。fork()函数会创建一个子进程,子进程是父进程的一个副本。此时,子进程和父进程拥有相同的代码段、数据段、堆栈段以及打开的文件描述符。
3.子进程执行新程序:在子进程中,fork()返回0,表示这是子进程。子进程通常会调用exec()系列函数来替换其地址空间中的代码和数据,以运行新的程序。例如,对于hello命令,子进程会调用execvp("hello", argv)来执行hello程序。
4.父进程等待子进程完成:默认情况下,父进程会调用wait()或waitpid()函数来等待子进程的完成。这些函数会使父进程暂停执行,直到子进程终止。这样,父进程可以获取子进程的退出状态,并进行相应的处理。
5.并发执行:虽然父进程和子进程是独立的进程,但它们共享同一个终端。因此,它们的输出可能会交织在一起。内核调度器负责管理这两个进程的执行,使得它们看起来像是并发运行的。
6.4 Hello的execve过程
1.删除现有内存段:当execve被调用时,操作系统首先会删除当前进程的所有内存段,包括代码段、数据段、堆和栈。这些段会被新的可执行文件的内容所替代。
2.创建新的内存段:操作系统为新的可执行文件分配新的内存段,包括代码段、数据段、堆和栈。这些段的大小和位置由可执行文件的头部信息决定。
3.初始化内存:新的堆和栈段会被初始化为零。这确保了未初始化的全局变量和局部变量都从零开始。
4.加载可执行文件内容:新的代码段和数据段会被填充为可执行文件中的内容。这意味着新的程序代码和静态数据会被加载到内存中。
5.设置程序计数器(PC):程序计数器(PC)被设置为指向新程序的入口点,通常是_start函数。这个函数是所有C语言程序的起始点,负责进行一些初始化工作,然后调用main函数。
6.开始执行新程序:一旦PC被设置好,CPU开始执行新程序的指令。此时,新程序的_start函数被调用,它最终会调用main函数,从而启动新程序的执行。
6.5 Hello的进程执行
1.进程调度的过程
调度时机:常见的调度时机有进程主动放弃 CPU(如执行 I/O 操作进入阻塞状态)、时间片用完、有更高优先级进程进入就绪队列等。假设 “Hello” 进程正在运行,此时有一个紧急任务对应的高优先级进程进入就绪队列,就会触发进程调度。
调度算法选择:操作系统会根据所采用的调度算法(如先来先服务、短作业优先、优先级调度、时间片轮转等)从就绪队列中挑选进程。若采用优先级调度算法,且新进入的高优先级进程优先级高于 “Hello” 进程,那么调度程序会选中高优先级进程。
上下文切换:确定新的运行进程后,进行上下文切换。首先保存当前运行进程(“Hello” 进程)的上下文信息,将寄存器内容存入 PCB 对应的区域,内核栈相关信息也妥善保存。然后恢复新选中进程的上下文信息,将其 PCB 中保存的寄存器值等恢复到相应寄存器中,更新程序计数器等,让新进程从上次中断处继续执行。
2.用户态与核心态转换
用户态:用户程序执行时处于用户态,在用户态下,程序只能访问受限的系统资源,不能直接执行特权指令(如修改内存映射、启动 I/O 设备等)。“Hello” 程序执行打印操作时,最初处于用户态,执行的是用户空间的代码。
核心态:操作系统内核程序运行在核心态,拥有对系统所有资源的访问权限,可以执行特权指令。当 “Hello” 程序需要进行打印操作时,会通过系统调用(如在 Linux 下的 write 系统调用)从用户态切换到核心态。系统调用会触发一个中断,CPU 接收到中断后,保存当前用户态的上下文信息,将处理器状态切换为核心态,然后跳转到内核中对应的中断处理程序。在内核中,中断处理程序会根据系统调用的参数进行相应的操作,如找到打印机设备驱动程序,安排打印任务等。完成系统调用相关操作后,再恢复之前保存的用户态上下文信息,将处理器状态切换回用户态,“Hello” 程序继续执行后续代码 。
6.6 hello的异常与信号处理
1.异常
图35 中断
中断异常:来自I/O设备的信号引起的异常,总是返回下一条指令
图36 陷阱
陷阱异常:执行一条指令的结果,总是返回下一条指令。
图37 故障
故障:潜在可恢复的错误导致的,可能返回当前指令。
图38 终止
终止:不可恢复的异常导致的,不会返回。
2.信号处理
(1)正常运行
图39 正常运行截图
(2)随机键入(不影响输出结果)
图40 运行中随便输入结果
(3)回车(会作为结束后下次指令的输入)
图41 运行中输入回车
(4)ctrl+c(进程终止)
图42 运行中输入ctrl+c
(5)ctrl+z(前台挂起)
图43 运行中输入ctrl+z
(6)ctrl+z后ps
图44 运行中输入ctrl+z后输入ps命令
(7)ctrl+z后jobs
图45 运行中输入ctrl+z后输入jobs命令
(8)ctrl+z后pstree
图46 运行中输入ctrl+z后输入pstree命令
(9)ctrl+z后fg
图47 运行中输入ctrl+z后输入fg命令
(10)ctrl+z后kill
图48 运行中输入ctrl+z后输入kill命令
6.7本章小结
本章介绍了进程的概念和作用,并简述了shell-bash的作用与处理流程以及hello的fork进程创建的过程,接着介绍了hello的execve过程,然后结合进程上下文信息讲述了程序的进程执行,最后介绍了hello的异常以及各种信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址:是程序编写和编译时使用的地址,由段选择子和段内偏移量组成。对于 hello 程序,在其代码中引用变量、函数等位置时产生的地址就是逻辑地址,它是相对的、与程序相关的地址,不对应实际物理内存位置 。
2.线性地址:经过段式管理转换后的地址。在 Intel 架构下,通过逻辑地址中的段选择子找到段描述符,结合段内偏移量计算得到线性地址。对于 hello 程序,逻辑地址经过段式管理单元转换后得到线性地址,它是一个连续的地址空间,但还不是实际物理内存地址。
3.虚拟地址:现代操作系统为每个进程提供的独立地址空间,进程看到的自己的内存地址。 hello 程序运行时,操作系统为其分配虚拟地址空间,程序中使用的地址都是虚拟地址,虚拟地址与物理地址通过页表进行映射转换。
4.物理地址:内存芯片上存储单元的实际地址。 hello 程序最终要访问物理内存中的数据和指令,经过虚拟地址到物理地址的转换(页式管理等机制)后,才能真正访问到物理内存位置。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel 架构中,逻辑地址由段选择子和段内偏移量组成。段式管理过程如下:
1. 获取段描述符:段选择子指向全局描述符表(GDT)或局部描述符表(LDT)中的一个段描述符。段描述符包含了段的基地址、段界限、段属性(如可读、可写、可执行等)等信息 。
2. 计算线性地址:将段描述符中的基地址与逻辑地址中的段内偏移量相加,得到线性地址。例如,若段描述符基地址为 Base ,段内偏移量为 Offset ,则线性地址 Linear Address = Base + Offset 。
7.3 Hello的线性地址到物理地址的变换-页式管理
1. 页表结构:操作系统维护页表,页表由页表项组成。每个页表项记录了一个虚拟页(对应线性地址划分的页)与物理页(对应物理内存划分的页)之间的映射关系,还包含一些标志位(如是否在内存、是否被修改等)。
2. 地址转换过程:对于 hello 程序的线性地址,将其划分为页号和页内偏移量。通过页号查找页表,得到对应的物理页框号,再将物理页框号与页内偏移量组合,就得到了物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
1.TLB:是一个高速缓存,用于缓存最近使用过的虚拟地址到物理地址的映射关系。当 hello 程序进行地址转换时,首先查询 TLB,如果命中(即找到对应的映射),则直接使用其中的物理地址,避免了访问页表的开销,大大提高了地址转换速度。
2.四级页表:现代操作系统(如 x86 - 64 架构)为了管理大的虚拟地址空间,采用四级页表结构。虚拟地址被划分为多个部分,每部分用于索引一级页表。从顶级页表开始,逐级通过索引找到下一级页表,最终在末级页表中找到物理页框号,与页内偏移量组合得到物理地址。当 TLB 未命中时,就需要通过四级页表逐步查找地址映射。
7.5 三级Cache支持下的物理内存访问
图49 三级缓存示意图
Cache结构:三级 Cache 分为 L1 Cache(一级缓存,速度最快,容量较小)、L2 Cache(二级缓存,速度和容量适中)、L3 Cache(三级缓存,容量较大,速度相对 L1、L2 稍慢) 。
访问过程:当 hello 程序需要访问物理内存时,首先检查 L1 Cache,如果数据在 L1 Cache 中(命中),则直接读取,速度极快;若未命中,则检查 L2 Cache,以此类推。如果在三级 Cache 中都未命中,才从主存(物理内存)中读取数据,然后将数据加载到 Cache 中,以便后续访问。通过这种方式,减少了对物理内存的直接访问次数,提高了程序运行效率。
7.6 hello进程fork时的内存映射
当 hello 进程执行 fork 系统调用时:
写时复制:父子进程最初共享相同的物理内存页面,它们的虚拟地址空间映射到相同的物理内存区域。此时,这些页面的属性被标记为只读。当父子进程中的任何一个进程试图修改共享页面中的数据时,操作系统会为修改进程分配新的物理内存页面,将原页面的数据复制到新页面,然后修改虚拟地址到物理地址的映射关系,使修改进程的虚拟地址指向新的物理页面,而未修改的进程仍共享原物理页面。
7.7 hello进程execve时的内存映射
当 hello 进程执行 execve 系统调用时:
旧内存清理:execve 会丢弃 hello 进程原有的用户空间内存映射,包括代码段、数据段、堆、栈等占用的内存区域。
新程序加载:将新程序(execve要执行的程序)的代码段、数据段等内容加载到内存中,并重新建立新的虚拟地址到物理地址的映射关系。新程序的代码被加载到代码段,初始化数据和未初始化数据分别加载到数据段和 bss 段,同时为程序的运行设置好栈和堆等内存区域。
7.8 缺页故障与缺页中断处理
缺页故障:当 hello 程序访问的虚拟地址对应的物理页面不在内存中(例如该页面被换出到磁盘)时,就会发生缺页故障。此时 CPU 会触发一个缺页中断。
缺页中断处理:操作系统的缺页中断处理程序被调用,它首先检查页表项,确定该页面是在磁盘上还是由于其他原因(如访问权限错误)导致缺页。如果页面在磁盘上,处理程序会将该页面从磁盘读入内存(可能需要置换内存中现有的页面以腾出空间),更新页表项中的相关信息(如将页面标记为在内存、更新访问和修改标志等),然后重新执行引发缺页故障的指令,使 hello 程序能够继续正常运行。
7.9动态存储分配管理
1. 分配方法
栈内存分配:由编译器自动管理,函数内局部变量等存储在栈上。在函数调用时分配空间,函数结束时自动释放,速度快。例如, int a; 这样在函数内部声明的变量,其内存就在栈上分配。
堆内存分配:通过 malloc (C 语言)、 new (C++ 语言)等函数手动分配。程序员需明确指定分配内存大小,使用完后通过 free (C 语言)、 delete (C++ 语言)手动释放。比如在 C 语言中, int *ptr = (int*)malloc(sizeof(int)); 就是在堆上分配一个 int 类型大小的内存空间。
2. 管理策略
首次适配:从空闲内存链表的起始位置开始查找,找到第一个能满足请求大小的空闲块,将其分配给请求者。优点是简单快速,倾向于使用内存低地址部分;缺点是可能会在低地址留下许多小的空闲块,导致后续大内存请求难以满足。
最佳适配:遍历整个空闲内存链表,找到最接近请求大小的空闲块进行分配。优点是能使剩下的空闲块尽可能小;缺点是每次分配都要遍历链表,开销大,且会产生大量难以利用的碎片。
伙伴系统:将内存按 2 的幂次方大小进行划分和管理。当有内存请求时,系统找到满足请求的最小 2 的幂次方大小的空闲块分配。如果没有合适大小的空闲块,就将大的空闲块分裂。优点是能有效减少内存碎片,分配和回收速度快;缺点是对内存大小要求严格,不能充分利用非 2 的幂次方大小的内存。
7.10本章小结
本章围绕 hello 程序,深入探讨了存储管理相关知识。从逻辑地址、线性地址、虚拟地址和物理地址的概念出发,介绍了 Intel 架构下从逻辑地址到线性地址的段式管理,以及从线性地址到物理地址的页式管理。还阐述了 TLB 与四级页表支持下虚拟地址到物理地址的变换、三级 Cache 对物理内存访问的作用。同时,对 hello 进程在 fork 和 execve 时的内存映射,缺页故障与缺页中断处理,以及动态内存分配管理等方面进行了剖析,全面展示了程序在内存中从地址映射到存储分配的运行机制。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章1分)
结论
通过对hello.c
从源代码到可执行文件的整个生命周期的深入探讨,我们系统地理解了计算机系统的基础原理和工作机制。
1.源代码到可执行文件的转换过程(P2P过程):
预处理:源代码文件 hello.c
经过预处理,生成包含所有头文件内容和宏展开后的文件 hello.i
。
编译:预处理后的代码通过编译器转换为汇编语言代码 hello.s
。
汇编:汇编器将汇编代码翻译成机器语言代码,生成可重定位的目标文件 hello.o
。
链接:链接器将多个目标文件和库文件链接在一起,生成最终的可执行文件 hello
。
2.程序的执行过程(020过程):
Shell解析:用户通过Shell输入命令,Shell解析命令行参数并调用 fork()
创建子进程。
子进程创建:子进程调用 execve()
加载并执行 hello
程序。
进程管理:操作系统管理进程的内存映射、时间片分配、上下文切换等。
资源回收:程序执行完毕后,操作系统回收进程占用的资源,包括内存、文件描述符等。
3.存储管理:
地址空间转换:逻辑地址、线性地址、虚拟地址和物理地址之间的转换机制,确保程序能够在虚拟内存环境中高效运行。
内存映射:fork()
和 execve()
系统调用时的内存映射机制,特别是写时复制(Copy-On-Write)和旧内存清理。
缺页处理:缺页故障和缺页中断处理机制,确保程序能够访问到所需的物理页面。
通过对上述各个阶段的详细分析,我们不仅掌握了计算机系统的工作原理,还深刻理解了程序从编写到执行的全过程。这一过程不仅涉及到编译、链接等技术细节,还包括操作系统对进程、内存和IO的管理机制。这些知识为我们提供了坚实的理论基础,有助于我们在未来的编程实践中更好地优化和调试程序。
深切感悟与创新理念
通过对 hello.c
程序的深入剖析,我深刻体会到计算机系统设计与实现的复杂性和精妙之处。每个阶段的处理都环环相扣,任何一个环节的疏忽都可能导致程序无法正常运行。因此,良好的编程习惯和对系统原理的深入理解至关重要。
在未来的设计与实现中,我们可以借鉴这些经验,探索更加高效的编译优化、内存管理和IO处理方法。例如,引入更智能的编译器优化算法,改进内存分配策略以减少碎片,以及优化IO路径以提高响应速度。这些创新理念将有助于推动计算机系统的发展,使其更加高效、稳定和安全。
附件
文件名 | 功能 |
hello.c | 源程序 |
hello.i | 预处理后得到的文件 |
hello.s | 编译后得到的汇编文件 |
hello.o | 汇编后的可重定位目标文件 |
hello.elf | 读取hello.o得到的ELF格式信息 |
helloo.elf | 读取hello得到的ELF格式信息 |
hello | 可执行文件 |
参考文献
[1] Randal E.Bryant David R.O’Hallaron. 深入理解计算机系统(第三版). 机械工业出版社,2016.