作为计算机系统课程的结业论文,本文章基于Computer Systems: A Programmer’s Perspective (3rd ed.),通过对一个简单的程序Hello在Linux环境下的生命周期的分析,论述其从hello.c经过预处理、编译、汇编、链接等一系列操作生成可执行文件hello,再通过程序对进程的管理、内存空间的分配、信号和异常的处理、对 I/O 设备的调用等环节彻底解释hello从创建到结束的过程,进而加深对计算机系统的理解。
关键词:程序生命周期;计算机系统;hello程序;预处理;编译;汇编;链接;
目 录
X64 CPU;2GHz;2G RAM;256GHD Disk
Windows 11 64位;Visual Studio 2022;Ubuntu 20.04 LTS 64位
Visual Studio 2022;gdb、edb等
表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后得到的反汇编文件 |
本章我们主要交代了hello的P2P和020过程,给出了论文研究时的环境与工具以及中间生成的文件信息。
预处理是编译器处理源代码的第一个阶段,它读取源代码文件(通常是.c、.cpp、.java等文件),并根据预处理指令(如#include、#define等)对源代码进行初步的修改或替换。预处理完成后,生成一个新的源代码文件(或直接在内存中处理),该文件将作为编译器的下一个阶段(编译阶段)的输入。
图2.1 cpp预处理命令效果
直接用cpp对hello.c文件进行预处理,会直接显示处理结果,因为需要使用重定向保存处理结果至hello.i(预处理之后的文件)中,具体如下:
图2.2 cpp预处理后保存至hello.i
对于预处理结果,我们可以直接使用VS打开hello.s文件查看,具体如下:
图2.3 hello.i中部分文件
图2.4 hello.i中部分宏定义
图2.5 hello.i中部分函数声明
图2.6 hello.i中的原始C代码部分
本章节我们回顾了预处理器的概念与主要作用,并对hello.c进行了预处理,最后对hello.i文件内容进行了简单分析。我们现在所得到的hello.i文件在下一章节会作为编译器(ccl)的输入文本。
第3章 编译
编译器(ccl)读取预处理后的文件,并将其转换为汇编语言源代码(通常是.s扩展名)。
图3.1 将hello.i编译为hello.s的命令
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.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 学号 姓名 手机号 秒数!”的机器代码,其被存储至内存中。
编译器将已初始化的全局和静态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 算术运算
此汇编代码中算数运算较为简单,主要有以下几种:
3.3.5 关系操作
此汇编代码中关系操作较少,主要有以下几种:
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 控制转移
此汇编代码中的控制转移语句较多,以下分析主要的典型:
25.je .L2 //两者相等,则跳转至 .L2处,对应图2.6中3051行。
32.jmp .L3 //是一种典型的“跳转至中间”的循环语句编译方法。
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
本章节我们探讨了编译器将预处理后的C程序hello.i翻译成汇编语言的过程,涉及各类数据、算术运算、关系操作、数组操作、控制转移和函数操作等方面。编译器还会优化程序,结果保存在hello.s文件中。该文件将作为下一章节汇编器(as)的输入文本。
第4章 汇编
汇编是将汇编语言源代码转换为机器语言目标代码的过程。汇编语言是一种低级编程语言,它使用助记符来代表机器指令,这使得编程者可以更容易地编写和理解代码,而无需直接处理二进制机器码。
图4.1 将hello.s汇编为hello.o的命令
图4.2 输出并打开ELF文件的命令
为了方便,我们可以直接用VS打开所生成的hello.elf文件,下文皆如此。
在 ELF文件中,ELF Header 是文件的第一个部分,它包含了关于整个文件的基本信息。对于此hello.o文件,以下是本ELF Header 包含的主要信息:
图4.3 ELF Header部分信息
此hello.o文件中,Section Headers提供了关于文件中各个节(Sections)的信息。每个节都包含特定类型的数据,如代码、数据、符号表等。以下是本Section Headers包含的信息:
图4.4 Section Headers部分信息
此hello.o文件中,Key to Flags用于解释在Section Headers中Flag字段的每一位所代表的含义。具体如下:
图4.5 Section Headers部分剩余信息
此hello.o文件中,Relocation section包含了关于如何修改文件中的代码和数据引用的信息,以便在链接时与其他目标文件或共享库中的符号进行正确的地址解析。以下是本Relocation section包含的信息:
图4.6 Relocation section部分信息
此hello.o文件中,Symbol table(符号表)包含了关于程序中定义和引用的函数、变量等符号的信息。符号表对于链接器(linker)和调试器(debugger)等工具来说是非常重要的,因为它们需要这些信息来解析符号引用和进行调试。以下是本Symbol table中包含的信息:
图4.7 Symbol table部分信息
本节中,我们对hello.o文件进行反汇编并输出hello.asm文件,并对hello.asm与hello.s文件内容进行对比分析,具体命令如下图:
图4.8 反汇编并输出hello.asm的命令
|
将hello.asm和hello.s进行比较,大部分相同,主要有一下几个方面不同:
机器语言和汇编语言之间的映射关系主要体现在汇编语言指令与机器语言指令的对应关系。汇编语言使用人类可读的助记符表示机器指令,而机器语言是计算机硬件直接执行的二进制指令集。
以此hello.asm为例:
任意指令之前所对应的数字即为其相对应的机器代码。汇编器通过查找指令集映射表将汇编指令转换为机器指令编码,并写入二进制文件。需注意不同CPU架构和指令集具有不同的汇编和机器语言规范,因此映射关系也不同。
本章我们讨论了汇编阶段将汇编代码hello.s翻译成机器语言指令hello,o的过程,并对汇编文件的产生进行了详细论述。同时,我们比较了汇编代码与反汇编代码之间的不同之处。这对于理解汇编语言的构成于生成机制大有裨益。本章所得到的hello.o文件将作为下一章节链接器(ld)的输入文本。
链接是将多个编译单元(通常是目标文件)合并成一个可执行文件或库文件的过程。在这个过程中,链接器(Linker)会解析符号引用,将定义在其他文件中的符号与在当前文件中使用的符号进行匹配,并生成最终的二进制文件。
图5.1 链接多个共享库生成hello的命令
此步我们可以将hello文件中ELF输出为单独文件,便于分析,命令如下:
图5.2 输出hello文件ELF部分的命令
而后我们可以用VS直接打开hello.elf2文件进行分析。
!!!声明!!!:此节内容多与4.3节内容相同,包括ELF Header、Section Headers、Relocation section与Symbol table部分,下文不再赘述。
对于不同部分的分析如下:
在hello.elf2文件中,每个 Program Header 都是一个 Elf64结构体的实例,它包含了以下字段:
图5.3 Program Headers部分信息
在hello.elf2文件中,Dynamic section包含了在运行时由动态链接器使用的信息。这些信息对于解析依赖项、初始化程序、以及处理动态链接过程中的其他任务至关重要。
Dynamic section是由一系列的Elf64结构体组成的,每个结构体都包含一个标签(Tag)和一个相关的值(Value)。这些标签和值定义了动态链接器所需的各种属性和行为。一些常见的Dynamic section标签包括:
图5.4 Dynamic section部分信息
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
程序被载入至地址0x401000~0x402000中。在该地址范围内,每个节的地址都与前一节中节对应的 Address 相同。通过ELF可知,程序从0x00401000到0x00400fff,在0x400fff之后存放的是.dynamic到.shstrtab节的内容。
本节中,我们对hello文件进行反汇编并输出hello.asm2文件,分析hello与hello.o的不同,并结合hello.o的重定位项目,阐述hello中重定位机制。具体命令如下图:
图5.6 反汇编并输出hello.asm2的命令
在链接过程中,重定位指的是将程序中的符号引用转换为它们在内存中的实际地址的过程。根据符号引用的类型和上下文,重定位可以分为几种不同的类型:
5.5.2 hello与hello.o文件的不同
hello.o与 hello在内容上有几个显著的不同点。以下是这些不同的主要方面:
5.5.3 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 |
GOT(全局偏移表)是存储共享库中全局变量和函数地址的全局数据结构。在运行时,GOT被初始化,各元素指向对应变量或函数地址。程序访问全局变量或函数时,先访问GOT地址,再跳转至实际地址。
GOT实现共享库动态链接,支持进程共享库,动态加载卸载。同时支持高级特性,如延迟绑定、符号重定位。
PLT(过程链接表)是动态链接器数据结构,用于运行时动态绑定函数调用。PLT通过跳转指令实现动态绑定,首次调用时保存真实地址至GOT。后续调用直接跳转至GOT中保存的函数地址。
本节我们使用gdb执行hello,在init处设置断点可以区分动态库链接的前后阶段,而后分别查询0x404000(详见图5.4 143行)可以得出其调用与跳转的各个子程序名和程序地址,具体如下:
图5.7 gdb 调试动态链接
本章我们探讨了链接的概念和作用,分析可执行文件hello的ELF格式及其虚拟地址空间,并对重定位过程、动态链接进行了深入的分析。
进程是操作系统进行资源分配和调度的基本单位,是操作系统结构的基础。它指的是一个程序在给定的工作空间和数据集上的一次运行活动。或者说,它是操作系统中执行的一个指令序列,是一条正在运行的程序。
6.1.2 进程的作用
6.2.2 Shell-bash的处理流程
在Linux系统中,fork进程创建过程可概述如下:
注意:父子进程拥有各自地址空间、文件描述符表和环境变量副本。子进程修改不影响父进程。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。
图6.1 程序不断fork的过程
execve是Linux操作系统中负责执行可执行文件的核心系统调用。在调用execve时,需传入文件路径、命令行参数以及环境变量等三个关键参数。一旦调用成功,它将全面替换当前进程的代码段为指定文件的执行内容,并依据全新的参数和变量配置来启动新程序的执行。
在实际应用中,通常会先通过fork系统调用创建一个子进程,随后在子进程中发起execve调用。execve的主要职责在于将目标程序加载至进程的地址空间,并精确设置程序计数器指向新程序的入口点。同时,它还会负责将命令行参数及环境变量等信息传递给新程序。
当加载器成功跳转到新程序的入口点时,即意味着进程的控制权已转移至新程序。新程序将从入口点开始执行,通常这个入口点对应于start地址。在start函数中,会进一步调用main函数,从而完成子进程中新程序的加载与初始化过程。
进程上下文信息是操作系统执行进程时记录的信息,包括:
进程时间片(Process Time Slice)是操作系统中用于多任务处理(也称为时间共享或多任务并发)的一个核心概念。在多任务操作系统中,CPU时间被划分为多个片段,每个片段称为一个时间片(Time Slice),并分配给不同的进程使用。
当一个进程获得CPU的使用权并开始执行时,它会被分配一个时间片。如果在这个时间片结束之前,进程没有完成它的任务或者因为某种原因(如等待I/O操作)而阻塞,那么CPU会停止执行该进程,并将CPU的使用权转交给其他等待执行的进程。当该进程再次变为可运行状态时(例如,I/O操作完成),操作系统会再次为它分配一个时间片,使其能够继续执行。
进程调度是操作系统在特定时刻进行的一项重要决策,旨在确定哪个进程将获得CPU的执行权。这一过程涉及对正在运行的进程进行优先级排序和调度执行,是操作系统对系统资源公平、高效分配的关键环节。通过合理的进程调度,操作系统能够显著提升系统的吞吐量和响应时间,从而确保系统运行的稳定性和效率。
6.5.4 用户模式和内核模式
用户模式是操作系统中为用户级应用程序提供的执行环境。在此模式下,应用程序代码被限制在有限的资源访问权限和受限的操作系统服务中。用户模式有如下特点:
内核模式是操作系统内核(Kernel)执行的特权模式。它允许无限制地访问系统内存、硬件和所有系统资源。内核模式有如下特点:
用户模式和内核模式是操作系统中两种重要的执行模式,它们通过系统调用来进行交互,共同实现了操作系统的安全性和稳定性。
异常可以分为四类:中断(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的结果图
本章我们主要讨论了进程和shell的概念与作用,详细论述了进程的创建和执行过程,以及对多种异常和信号的处理有了直观的认识。
线性地址是逻辑地址到物理地址转换过程中的一个中间层。在分段机制中,逻辑地址(段中的偏移地址)加上段基址(段地址)就形成了线性地址。如果“hello”字符串在程序的某个段中,它的逻辑地址加上该段的基地址就会形成一个线性地址。例如,如果逻辑地址是0x1000,段基址是0x200000,那么线性地址就是0x201000。线性地址是一个连续的、一维的地址空间,用于简化内存管理。
页式管理是一种内存空间存储管理的技术,它将各进程的虚拟空间划分成若干个长度相等的页(page),同时把内存空间也按页的大小划分成片或页面(page frame)。页式管理通过建立页表来实现虚拟地址与物理地址之间的一一对应。
线性地址由高位的“页目录索引”、中位的“页表索引”和低位的“页内偏移量”三部分组成。这三部分分别用于定位页目录、页表和页内的具体位置。
从线性地址变换到物理地址的过程具体如下:
TLB是硬件高速缓存,加速虚拟地址(VA)到物理地址(PA)的翻译。四级页表机制下,虚拟地址需四级映射得物理地址,访问内存降低性能。TLB缓存常用虚拟地址到物理地址映射,若CPU访问的虚拟地址在TLB中,则直接得物理地址,避免多次访问内存。
四级页表机制中,虚拟地址32位,高10位页目录号,中10位页表号,低12位页内偏移量。CPU根据页目录号和页表号查页目录表和页表,得物理地址对应的页框号和偏移量,加页内偏移量得物理地址。若TLB无缓存对应映射,则经四级映射过程。若TLB有缓存映射,则直接从TLB得物理地址,此过程快于四级映射。
在三级Cache支持下,CPU首先会在L1 Cache中查找数据,若未找到则继续在L2 Cache中查找,仍未找到则进一步在L3 Cache中查找。若三级Cache中均未找到,则从主存中获取并存储到L3 Cache中以加速下次访问。
在物理内存访问时,CPU通过地址总线将地址发送到内存控制器。内存控制器解码地址,找到并读取所需物理内存数据,同时将其缓存到L3 Cache中以加速后续访问。
在hello进程使用fork()系统调用创建子进程时,内存映射的过程可以概括为以下几个关键步骤:
当hello进程执行execve系统调用时,内存映射会经历一系列的变化。以下是对execve时内存映射变化的具体描述:
hello进程在执行过程中可能会遇到缺页故障和缺页中断。以下是关于缺页故障与缺页中断处理的机制阐述:
当进程试图访问其地址空间中的一个页面时,该页面当前并不在物理内存中(即存在位为0),此时CPU会停止当前指令的执行,并通知操作系统内核产生了一个缺页故障。
缺页故障的处理:处理器生成一个虚拟地址,并将它传送给内存管理单元(MMU)。MMU生成PTE地址,并从高速缓存/主存请求得到它。高速缓存/主存向MMU返回PTE。如果PTE中的有效位是0(表示页面不在内存中),则MMU触发一次异常,将CPU中的控制转移到操作系统内核中的缺页异常处理程序。缺页处理程序会确定需要从磁盘加载哪个页面,并将其加载到物理内存中。最后缺页处理程序返回到原来的进程,继续执行导致缺页的命令。
操作系统内核响应缺页故障时产生的一种特殊类型的中断。当CPU检测到缺页故障时,它会生成一个缺页中断,并将控制权转移到操作系统内核的缺页中断处理程序中。
缺页中断的处理:缺页中断的处理过程与缺页故障的处理过程非常相似,因为两者都是由于页面不在物理内存中而导致的。在处理缺页中断时,操作系统内核会执行以下步骤:
在本hello进程中,动态内存管理的基本方法与策略主要包括以下几个方面:
基本方法:
malloc/calloc/realloc :用于在堆上动态分配内存。malloc用于指定大小的内存块分配;calloc会额外将分配的内存初始化为零;realloc用于调整已分配内存块的大小。
策略:
根据程序运行时的实际需求动态分配内存,避免静态分配导致的内存浪费或不足。使用malloc或new时,应检查返回值是否为NULL,以确保内存分配成功。
7.9.2 动态内存释放
基本方法:
free (C语言):用于释放之前通过malloc、calloc或realloc分配的内存。
策略:
一旦不再需要动态分配的内存,应立即释放,以避免内存泄漏。释放内存后,应将指向该内存的指针置为NULL,防止野指针的出现。
本章节我们探讨了hello的存储管理。从hello的存储器地址空间起,我们逐步对线性地址到物理地址的翻译进行了详尽叙述。并且在内存映射的角度上回顾了fork和execve函数。而后又介绍了缺页故障和缺页中断处理机制,最后介绍了动态存储分配管理机制。
Linux的IO设备管理方法主要基于Unix的IO接口,并在此基础上进行了扩展和优化。主要部分如下:
Unix系统提供了多种I/O模型,包括:
8.2.2 Unix I/O接口函数
Unix I/O接口函数主要包括以下几个:
printf函数的实现涉及多个步骤:从vsprintf生成显示信息、write系统函数再到系统调用。接着,字符显示驱动子程序将ASCII转换为字模库,并存储在vram中(包含每个点的RGB信息)。最后,显示芯片按刷新频率从vram读取数据,并通过信号线将每个点的RGB分量传输到液晶显示器。以下从这三部分进行阐述:
从 vsprintf 生成显示信息、write 系统函数再到陷阱-系统调用
字符显示驱动子程序:ASCII到字模库到显示VRAM
显示芯片按刷新频率从VRAM读取数据,通过信号线传输给液晶显示器,控制每个像素的亮度和颜色,显示完整图像。
getchar 函数是 C 标准库中的一个函数,用于从标准输入(通常是键盘)读取一个字符。在 Unix 和 Linux 系统中,getchar 的实现通常与底层的键盘中断处理、系统调用以及用户空间与内核空间的交互密切相关,以下将从两个阶段分析getchar函数的实现:
异步异常-键盘中断的处理
getchar等调用read系统函数
本章节我们探讨了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。
在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.