计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业
学 号
班 级
学 生
指 导 教 师 史先俊
计算机科学与技术学院
2023年4月
本文将对经典程序hello world的运行全过程进行分析,完整地呈现出hello world的生命周期。经过预处理、编译、汇编、链接,最终形成可执行目标文件hello,由存储器保存在磁盘中。运行进程时,操作系统为其分配虚拟地址空间,提供异常控制流等强大的工具,Unix I/O为其提供与程序员和系统文件交互的方式。本文将以文件hello.c为示例文件,进行如上内容的详细讲解。
关键词:全过程,hello,计算机系统
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
7.1 hello的存储器地址空间............................................................................ - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................ - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理....................................... - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换............................................. - 11 -
7.5 三级Cache支持下的物理内存访问.......................................................... - 11 -
7.6 hello进程fork时的内存映射.................................................................. - 11 -
7.7 hello进程execve时的内存映射.............................................................. - 11 -
7.8 缺页故障与缺页中断处理........................................................................... - 11 -
8.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
1.1 Hello简介
1.1.1 Hello的P2P过程
P2P指的是Program to Process。在Linux系统中,源代码hello.c经过预处理,生成文本文件hello.i,然后经过编译生成汇编程序文件hello.s,之后hello.s通过汇编生成可重定位的目标代码hello.o,然后由链接器生成可执行程序文件hello。在shell中运行hello的命令后,操作系统为shell创建子进程,并将hello加载到其中运行。这就是Hello的P2P过程。
1.1.2 Hello的020过程
020指的是从“零”到“零”。当hello未运行时,其不占用任何内存空间。一旦准备开始运行,系统便为其分配各种资源。首先调用fork创建一个新进程,然后调用execve将hello加载并运行。当hello终止,其进程被shell回收,内存占用被清除,至此“空无一物”,这就是Hello的020过程。
1.2 环境与工具
硬件环境: Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz 2.40 GHz
软件环境:Windows 10 + 虚拟机Vmware (Ubuntu 20.04.4 LTS)
开发和调试工具:GCC,EDB,Clion,sublime
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
1.4 本章小结
本章给出了Hello的P2P和020过程的解释,给出了本文所使用的环境与工具,并展示出本文实验过程中所生成的中间结果文件的名字、作用等。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理是指在程序编译之前对源代码进行处理的一种过程。在预处理阶段,编译器会通过一系列操作,对源代码中的特定指令或者语句进行处理或者替换,以生成最终的可执行代码。
在C语言中,预处理是指在编译器进行编译之前,对源代码进行的一系列文本替换、文件包含、条件编译等处理。预处理器会找到程序中用# 开头的预处理指令并执行相应的操作,这些指令以“#”字符开头,直到换行符或文件结束符结束。
作用:一般而言,预处理器会执行一些宏定义、条件编译、头文件包含、常量声明等操作。预处理可以让程序员在代码中使用更高层次的语法结构或者实现更复杂的操作,同时也可以使代码更加清晰易读、可维护性更好。
C语言的预处理器主要有以下几个作用:
- 宏定义:使用 #define 指令定义宏,可以将某个表达式或者代码块赋予一个名称,在代码中多次用到时可以简化处理,提高代码可读性和维护性。
- 文件包含:使用 #include 指令将其他源文件引入到当前文件中,方便代码重用和管理。
- 条件编译:使用 #if、#ifdef 等指令实现根据条件编译的功能,可针对不同的操作系统、平台、编译器等不同的条件选择编译不同的代码。
- 提供注释:使用 # 注释表示注释掉本行的代码,方便程序员调试代码和记录代码信息。
2.2在Ubuntu下预处理的命令
$ gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
运行结果截图如上,生成了hello.i文件,对hello.i文件进行查看
可以看到原本几行的文件扩展成了3060行,并将头文件,函数声明等进行了展开替换。
2.4 本章小结
本章讲述了预处理的概念和作用,尤其讲述了C语言相关的具体信息。接着给出了在Ubuntu下hello.c的预处理命令,并对得到的预处理后的文件hello.i进行查看与简要分析。
第3章 编译
3.1 编译的概念与作用
概念:
编译是将源代码转换为目标代码的过程,在C语言中,编译操作由编译器完成。通常地,编译的过程可以分为词法分析、语法分析、语义检查、代码生成等几个阶段。
在编译过程中,编译器首先会对源代码进行词法分析,将源代码分解为词法单元(如关键字、标识符、运算符等),然后进行语法分析,检查代码的语法正确性,并将代码构建成语法树。接着,编译器进行语义检查,检查代码的语义正确性,例如变量类型是否匹配、函数调用是否合符规范等等。最后,编译器会生成目标代码(即机器码),并将其存储到可执行文件中。
C语言的编译器有许多种,如gcc、clang等,他们都有自己的编译器工具链,可以进行编译、链接、优化等操作。
编译的作用:
- 将高级语言转换为机器码:计算机只能识别二进制代码(如01010..),而C语言属于高级语言,需要通过编译器将代码转换为机器码,使计算机能够识别和运行。
- 检查错误和提供优化:编译器可以检查代码中的错误、提供优化、压缩代码等操作,以提高程序的性能和效率。
- 支持跨平台:C语言源代码一般是可移植的,通过编译器可以将源代码编译成不同平台的可执行文件,从而使得程序在不同的平台上都能够运行。
- 便于开发和维护:通过编译,我们可以将代码转换为可执行形式,方便我们进行测试、调试和部署,同时也可以更好地管理和维护代码。
3.2 在Ubuntu下编译的命令
$ gcc –m64 –no-pie –fno-PIC -S hello.c -o hello.s
3.3 Hello的编译结果解析
-
-
- 编译代码介绍
-
.file:声明源文件
.text:代码节
.section:
.rodata:只读代码段
.align:数据或者指令的地址对其方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型
-
-
- 常量(赋值)
- 字符串常量
- 常量(赋值)
-
LC0,LC1两个原本为printf中的语句被存放在LCx处
-
-
-
- 局部变量的初始化值
-
-
通过movl赋值给寄存器rbp,对应源代码中循环中的i
-
-
-
- 条件判断边界
-
-
Cmpl语句做比较,对应原代码if(argc!=4){
效果类似上,对i进行判断,i<5
-
-
-
- 参数
-
-
对应exit中的参数1
-
-
- 变量
-
(1)argc, argv
如图,初始的两者被放在edi和rsi中,之后又被复制到-20(%rbp)和-32(%rbp)的位置
如图为argc的取用,argv与之类似
(2)i
如图为i的三次调用,分别是初始化,循环增加,判断
-
-
- 类型转换
-
(1)
对-20(%rbp)进行类型强制转换为int型后与4进行比较
(2)
将该值强制转换为指向字符型的指针,再赋值给%rsi。
-
-
- 赋值和算术操作
-
- 赋值
直接覆盖
- 算术操作
对变量i进行+1操作
-
-
- 关系操作(控制转移)
-
(1)
将其与4比较
(2)
跳转至L2
(3)
将其与4比较
(4)
比较后进行跳转
-
-
- 数组/指针/结构操作
-
定义两个字符串常量数组:
.LC0:
.string "hello"
.LC1:
.string "world"
把%rsi寄存器所指向的第一个函数参数(char *argv[])存储到-32(%rbp)的位置:
movq %rsi, -32(%rbp)
使用指针运算符"*"取得%rax寄存器指向的值:
movq (%rax), %rcx
使用指针运算符"+"将%rax寄存器指向的地址增加了16个字节:
addq $16, %rax
通过指针解引用和指针运算符"++"来访问数组元素并输出:
movsbl (%rdx,%rax),%eax
将字符串转换成整型数值:
callq atoi
使用指针运算符"+"将%rax寄存器指向的地址增加了24个字节:
addq $24, %rax
使用指针运算符"*"取得%rax寄存器指向的值,并将其作为sleep函数的参数:
movq (%rax), %rax
-
-
- 函数操作
-
调用atoi函数将字符串转换成整型数值:
callq atoi
调用sleep函数并将前面得到的整型数值作为参数:
callq
sleep
这两行代码分别调用了C标准库中的函数atoi和sleep
3.4 本章小结
本章首先给出编译的概念与作用,接着给出在Ubuntu中编译的命令,最后依照大作业PPT第四页的要求,分块地对编译后的hello.s文件进行了分析解释。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编语言是一种低级别的计算机语言,它是高级语言和计算机硬件之间的翻译语言。计算机需要使用二进制代码或者机器码来运行程序,汇编语言就是将人类可读的指令(如MOV、ADD等)转换成计算机可读的01序列。它在计算机系统中的位置位于高级语言和机器语言之间,因为它比机器语言更容易理解,同时又可以很方便地与高级语言交互。
作用:将人类可读的指令(如MOV、ADD等)转换成计算机可读的01序列。本文中指的是由Hello.s文件生成Hello.o文件的过程。
4.2 在Ubuntu下汇编的命令
-
-
- 命令
-
$ gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
-
-
- 指令
-
$ readelf -a hello.o > hello_elf.txt
将hello.o的ELF信息输出至hello_elf文本文件中
-
-
- 分析ELF信息
-
ELF 头: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 类别: ELF64 数据: 2 补码,小端序 (little endian) Version: 1 (current) OS/ABI: UNIX - System V ABI 版本: 0 类型: REL (可重定位文件) 系统架构: Advanced Micro Devices X86-64 版本: 0x1 入口点地址: 0x0 程序头起点: 0 (bytes into file) Start of section headers: 1192 (bytes into file) 标志: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 14 Section header string table index: 13 |
分析:
- Magic为16字节的序列,45 4c 46分别对应了ASCII码中的字母E、L和F,表明这是一个elf文件;02与“类别”对应,标明机器是64位;01 01分别对应“数据”和“Version”的相应值。
- 在“类型”处可知此目标文件为可重定位文件
- 在“系统架构处”可知机器类型为x86-64
- 其他内容类似如上分析
-
- 节头
-
节头: [号] 名称 类型 地址 偏移量 大小 全体大小 旗标 链接 信息 对齐 [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 000000000000008e 0000000000000000 AX 0 0 1 [ 2] .rela.text RELA 0000000000000000 00000358 00000000000000c0 0000000000000018 I 11 1 8 [ 3] .data PROGBITS 0000000000000000 000000ce 0000000000000000 0000000000000000 WA 0 0 1 [ 4] .bss NOBITS 0000000000000000 000000ce 0000000000000000 0000000000000000 WA 0 0 1 [ 5] .rodata PROGBITS 0000000000000000 000000d0 0000000000000033 0000000000000000 A 0 0 8 [ 6] .comment PROGBITS 0000000000000000 00000103 000000000000002c 0000000000000001 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 0000000000000000 0000012f 0000000000000000 0000000000000000 0 0 1 [ 8] .note.gnu.propert NOTE 0000000000000000 00000130 0000000000000020 0000000000000000 A 0 0 8 [ 9] .eh_frame PROGBITS 0000000000000000 00000150 0000000000000038 0000000000000000 A 0 0 8 [10] .rela.eh_frame RELA 0000000000000000 00000418 0000000000000018 0000000000000018 I 11 9 8 [11] .symtab SYMTAB 0000000000000000 00000188 0000000000000198 0000000000000018 12 10 8 [12] .strtab STRTAB 0000000000000000 00000320 0000000000000032 0000000000000000 0 0 1 [13] .shstrtab STRTAB 0000000000000000 00000430 0000000000000074 0000000000000000 0 0 1 |
分析:
- 从“地址”可知,各节的起始地址均为0,便于重定位
- 从“大小”可知,.data和.bss的大小都为0,这是由于本程序并没有全局变量
- 从“旗标”可知,.data可写但只读数据;.rodata不可写;.text可执行但不可写
-
- 重定位节
-
重定位节 '.rela.text' at offset 0x358 contains 8 entries: 偏移量 信息 类型 符号值 符号名称 + 加数 00000000001a 00050000000a R_X86_64_32 0000000000000000 .rodata + 0 00000000001f 000b00000004 R_X86_64_PLT32 0000000000000000 puts - 4 000000000029 000c00000004 R_X86_64_PLT32 0000000000000000 exit - 4 000000000050 00050000000a R_X86_64_32 0000000000000000 .rodata + 26 00000000005a 000d00000004 R_X86_64_PLT32 0000000000000000 printf - 4 00000000006d 000e00000004 R_X86_64_PLT32 0000000000000000 atoi - 4 000000000074 000f00000004 R_X86_64_PLT32 0000000000000000 sleep - 4 000000000083 001000000004 R_X86_64_PLT32 0000000000000000 getchar - 4 重定位节 '.rela.eh_frame' at offset 0x418 contains 1 entry: 偏移量 信息 类型 符号值 符号名称 + 加数 000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0 |
分析:共有8条,其中类型为R_X86_64_32的条目引用方式为绝对引用,对应两个字符串常量;R_X86_64_PLT32则是PC相对引用,对应main中的6个函数。跳转指令中地址的长度为4个字节,故重定位时需-4可到达下一条指令地址
-
-
- 符号表
-
Symbol table '.symtab' contains 17 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000000 0 SECTION LOCAL DEFAULT 7 7: 0000000000000000 0 SECTION LOCAL DEFAULT 8 8: 0000000000000000 0 SECTION LOCAL DEFAULT 9 9: 0000000000000000 0 SECTION LOCAL DEFAULT 6 10: 0000000000000000 142 FUNC GLOBAL DEFAULT 1 main 11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts 12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit 13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf 14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND atoi 15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sleep 16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND getchar |
分析:符号表中存放函数和全局变量的信息。
4.4 Hello.o的结果解析
4.4.1 指令objdump -d -r hello.o
如下为文件内容:
hello.o: 文件格式 elf64-x86-64 Disassembly of section .text: 0000000000000000 <main>: 0: f3 0f 1e fa endbr64 4: 55 push %rbp 5: 48 89 e5 mov %rsp,%rbp 8: 48 83 ec 20 sub $0x20,%rsp c: 89 7d ec mov %edi,-0x14(%rbp) f: 48 89 75 e0 mov %rsi,-0x20(%rbp) 13: 83 7d ec 04 cmpl $0x4,-0x14(%rbp) 17: 74 14 je 2d <main+0x2d> 19: bf 00 00 00 00 mov $0x0,%edi 1a: R_X86_64_32 .rodata 1e: e8 00 00 00 00 callq 23 <main+0x23> 1f: R_X86_64_PLT32 puts-0x4 23: bf 01 00 00 00 mov $0x1,%edi 28: e8 00 00 00 00 callq 2d <main+0x2d> 29: R_X86_64_PLT32 exit-0x4 2d: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 34: eb 46 jmp 7c <main+0x7c> 36: 48 8b 45 e0 mov -0x20(%rbp),%rax 3a: 48 83 c0 10 add $0x10,%rax 3e: 48 8b 10 mov (%rax),%rdx 41: 48 8b 45 e0 mov -0x20(%rbp),%rax 45: 48 83 c0 08 add $0x8,%rax 49: 48 8b 00 mov (%rax),%rax 4c: 48 89 c6 mov %rax,%rsi 4f: bf 00 00 00 00 mov $0x0,%edi 50: R_X86_64_32 .rodata+0x26 54: b8 00 00 00 00 mov $0x0,%eax 59: e8 00 00 00 00 callq 5e <main+0x5e> 5a: R_X86_64_PLT32 printf-0x4 5e: 48 8b 45 e0 mov -0x20(%rbp),%rax 62: 48 83 c0 18 add $0x18,%rax 66: 48 8b 00 mov (%rax),%rax 69: 48 89 c7 mov %rax,%rdi 6c: e8 00 00 00 00 callq 71 <main+0x71> 6d: R_X86_64_PLT32 atoi-0x4 71: 89 c7 mov %eax,%edi 73: e8 00 00 00 00 callq 78 <main+0x78> 74: R_X86_64_PLT32 sleep-0x4 78: 83 45 fc 01 addl $0x1,-0x4(%rbp) 7c: 83 7d fc 04 cmpl $0x4,-0x4(%rbp) 80: 7e b4 jle 36 <main+0x36> 82: e8 00 00 00 00 callq 87 <main+0x87> 83: R_X86_64_PLT32 getchar-0x4 87: b8 00 00 00 00 mov $0x0,%eax 8c: c9 leaveq 8d: c3 retq |
4.4.2 分析:
4.4.2.1 与.o文件进行对照
(1)机器语言由指令码和操作数构成
(2)机器语言与汇编语言并不是一一对应的映射关系
(3)在分支转移指令处,hello.s通过标号来指明跳转地址,而机器语言中是由目标地址与当前PC值作差获得,即 PC相对寻址
(4)在函数调用时,由于尚未链接,故操作数都为0。在反汇编文件中,在调用的下一行紧接着操作数的寻址方式和存放地址,而汇编文件中则是call函数名
4.5 本章小结
本章介绍了汇编的概念以及在本文中的作用,通过使用在Ubuntu下汇编的命令进行汇编,进而获取其ELF信息进行分析,最后再将反汇编的文件与hello.s进行对照,展现了机器语言和汇编语言的差异。
第5章 链接
5.1 链接的概念与作用
概念:链接(Linking)是将多个目标文件合并为一个可执行文件的过程。当一个程序被编写后,它通常会被分成多个源文件,每一个源文件最终编译生成一个目标文件(.o
或者 .obj
文件)。这些目标文件包含了代码和数据,但是它们还没有能力直接被操作系统或者硬件执行。在这种情况下,就需要对这些目标文件进行链接,以生成一个完整的可执行程序。
作用:
- 解析符号引用:多个目标文件之间会存在相互引用的情况,例如一个源文件中调用了另外一个源文件中定义的函数,或者在不同的源文件中定义了同一个全局变量。在链接过程中,链接器会解析这些符号的引用,确定它们在程序中实际的地址,并将它们正确地链接到一起。
- 合并代码段和数据段:不同的目标文件可能会包含相同的代码段和数据段,例如 C 标准库中的函数等。在链接过程中,链接器会将这些代码段和数据段合并在一起,从而避免在运行时出现冲突。
- 生成可执行文件:经过链接后,多个目标文件被合并成为一个可执行文件,其中包含了程序的所有代码和数据,可以被操作系统或硬件直接执行。
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.3 可执行目标文件hello的格式
命令:readelf -a hello > hello_elf.txt
通过该指令获得hello的ELF信息,并将输出放入hello_elf.txt中,下面对其进行分析
5.3.1 ELF头
ELF 头: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 类别: ELF64 数据: 2 补码,小端序 (little endian) Version: 1 (current) OS/ABI: UNIX - System V ABI 版本: 0 类型: EXEC (可执行文件) 系统架构: Advanced Micro Devices X86-64 版本: 0x1 入口点地址: 0x4010f0 程序头起点: 64 (bytes into file) Start of section headers: 14208 (bytes into file) 标志: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 12 Size of section headers: 64 (bytes) Number of section headers: 27 Section header string table index: 26 |
分析:与hello.o不同的是,在经过链接后,“类型”变为EXEC可执行文件
5.3.2 节头
节头: [号] 名称 类型 地址 偏移量 大小 全体大小 旗标 链接 信息 对齐 [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 00000000004002e0 000002e0 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.gnu.propert NOTE 0000000000400300 00000300 0000000000000020 0000000000000000 A 0 0 8 [ 3] .note.ABI-tag NOTE 0000000000400320 00000320 0000000000000020 0000000000000000 A 0 0 4 [ 4] .hash HASH 0000000000400340 00000340 0000000000000038 0000000000000004 A 6 0 8 [ 5] .gnu.hash GNU_HASH 0000000000400378 00000378 000000000000001c 0000000000000000 A 6 0 8 [ 6] .dynsym DYNSYM 0000000000400398 00000398 00000000000000d8 0000000000000018 A 7 1 8 [ 7] .dynstr STRTAB 0000000000400470 00000470 000000000000005c 0000000000000000 A 0 0 1 [ 8] .gnu.version VERSYM 00000000004004cc 000004cc 0000000000000012 0000000000000002 A 6 0 2 [ 9] .gnu.version_r VERNEED 00000000004004e0 000004e0 0000000000000020 0000000000000000 A 7 1 8 [10] .rela.dyn RELA 0000000000400500 00000500 0000000000000030 0000000000000018 A 6 0 8 [11] .rela.plt RELA 0000000000400530 00000530 0000000000000090 0000000000000018 AI 6 21 8 [12] .init PROGBITS 0000000000401000 00001000 000000000000001b 0000000000000000 AX 0 0 4 [13] .plt PROGBITS 0000000000401020 00001020 0000000000000070 0000000000000010 AX 0 0 16 [14] .plt.sec PROGBITS 0000000000401090 00001090 0000000000000060 0000000000000010 AX 0 0 16 [15] .text PROGBITS 00000000004010f0 000010f0 0000000000000145 0000000000000000 AX 0 0 16 [16] .fini PROGBITS 0000000000401238 00001238 000000000000000d 0000000000000000 AX 0 0 4 [17] .rodata PROGBITS 0000000000402000 00002000 000000000000003b 0000000000000000 A 0 0 8 [18] .eh_frame PROGBITS 0000000000402040 00002040 00000000000000fc 0000000000000000 A 0 0 8 [19] .dynamic DYNAMIC 0000000000403e50 00002e50 00000000000001a0 0000000000000010 WA 7 0 8 [20] .got PROGBITS 0000000000403ff0 00002ff0 0000000000000010 0000000000000008 WA 0 0 8 [21] .got.plt PROGBITS 0000000000404000 00003000 0000000000000048 0000000000000008 WA 0 0 8 [22] .data PROGBITS 0000000000404048 00003048 0000000000000004 0000000000000000 WA 0 0 1 [23] .comment PROGBITS 0000000000000000 0000304c 000000000000002b 0000000000000001 MS 0 0 1 [24] .symtab SYMTAB 0000000000000000 00003078 00000000000004c8 0000000000000018 25 30 8 [25] .strtab STRTAB 0000000000000000 00003540 0000000000000158 0000000000000000 0 0 1 [26] .shstrtab STRTAB 0000000000000000 00003698 00000000000000e1 0000000000000000 0 0 1 |
分析:节头列出了各段的基本信息
5.4 hello的虚拟地址空间
命令:./edb --run hello
使用edb加载hello,界面如上所示,Data Dump处可查看虚拟空间内容,其与节头有对应关系,以init段为例:
[12] .init PROGBITS 0000000000401000 00001000 000000000000001b 0000000000000000 AX 0 0 4 |
节头中该内容含义为init起始地址为0x401000,大小为0x1b。
在edb的Data Dump中寻找此地址,内容如下
由图可知该字段的内容为init,其他段与init类似。
5.5 链接的重定位过程分析
指令:objdump -d -r hello > hello_dis.txt
将结果输入hello_dis.txt中。
5.5.1 下面对其内容进行分析:
(1)hello相较于hello.o,多了.init, .plt, .plt.sec, .fini
(2)hello.o中,可重定位条目的地址对应操作数为0用于占位,而hello中 替换为了具体的值
(3)在hello.o中只有main函数有定义。而在hello中,所有调用的函数都被连接起来,且都位于.plt.sec中
5.5.2连接的过程
链接(Linking)是将多个目标文件合并为一个可执行文件的过程。链接过程通常包括以下几个步骤:
- 符号解析:在链接过程中,首先需要解析程序中的符号引用,即确定函数和变量在程序中的实际地址。对于每个目标文件,链接器会读取其中的符号表,并将其中定义的符号插入到全局符号表中。然后,链接器会遍历全局符号表,解析所有未定义的符号,以确定它们的实际地址。
- 重定位:在解析符号定义和引用之后,链接器需要将目标文件中的代码和数据段放置到正确的内存位置上。这个过程称为重定位。链接器会根据符号表信息、目标文件的段信息以及系统内存布局等因素,计算出目标文件中每个符号的实际地址,并将其修改为正确的值。
- 合并代码段和数据段:在重定位之后,链接器需要将多个目标文件中的代码段和数据段进行合并。这个过程中,链接器会处理公共段(Common Section)、重复定义(Duplicate Definition)等问题,并将相同的段进行合并或者去重。
- 生成可重定位文件或可执行文件:在以上步骤完成之后,链接器会生成最终的可重定位文件或可执行文件。可重定位文件主要用于静态库的生成,而可执行文件则是最终用户可以直接运行的程序。
hello的重定位:hello将两个字符串常量的占位操作数0替换为了其绝对地址
如图111和124行。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
初始:
步入:
call进入了libc-2.31.so!__libc_start_main
进而又进入hello!main
在main中顺序调用了一系列函数
Hello!printf@plt
hello!atoi@plt
Hello!sleep@plt
hello!getchar@plt
最终,调用到libc-2.31.so!exit,退出程序。
5.7 Hello的动态链接分析
在动态链接前将重定位与符号表信息链接进来,生成的hello本身是一个部分链接的可执行目标文件;只有当hello被加载时,动态链接器才将剩余的代码和数据链接进来,这就是“加载时动态链接”。
观察.got中动态链接项目在dl_init前后的变化
之前:
之后:
5.8 本章小结
本章介绍了链接的概念和作用,展示了在Ubuntu下链接的命令,展示了hello的格式并进行对照分析差异。展示了hello的虚拟地址空间,对链接的重定位过程进行分析,对hello的执行流程进行展示,对Hello的动态链接进行分析。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是计算机中正在运行的程序的实例。它是操作系统中的基本单位,由操作系统负责进程的管理和调度。每个进程都拥有独立的内存空间、程序计数器、寄存器等资源,并能够与其他进程进行通信。
作用:是实现操作系统中的多任务处理。在计算机中,多个程序可以同时运行,但计算机只有一个CPU,进程通过操作系统的调度机制,将CPU时间分配给各个程序,让它们看起来像是同时运行。这种方式可以提高计算机的利用率,提高工作效率。此外,进程还可以实现进程间通信,允许不同进程之间进行数据交换和共享资源,以实现更复杂的功能。
6.2 简述壳Shell-bash的作用与处理流程
壳(Shell)是一种命令解释器,负责接收用户输入的命令并转换成内核可执行的指令。Bash是常用的壳程序之一,是Linux和macOS等操作系统默认的壳程序。
壳的主要作用是提供用户与操作系统交互的接口,使用户可以通过命令行界面(CLI)或脚本编写执行指令来操作计算机。相比图形界面(GUI),命令行界面更加高效、灵活。
壳处理流程如下:
- 用户在命令行界面中输入命令;
- Shell读取用户输入的命令;
- Shell对用户输入的命令进行解析和分析;
- Shell识别命令中的关键字,命令名以及参数,并检查语法是否正确;
- 如果命令含有环境变量或通配符等特殊字符,Shell会先对它们进行替换;
- Shell根据命令类型判断需要执行的程序文件;
- 如果是内部命令(如echo、cd等),Shell会直接在自身进程中执行;
- 如果是外部命令(如ls、ps等),Shell会创建一个新的子进程(fork进程),在子进程中执行该命令;
- 子进程执行完毕后,Shell会将执行结果返回给用户;
- 如果命令含有输出重定向、管道等操作,Shell会先处理这些操作符,再执行相应的命令。
Bash支持多种类型的命令和参数,包括标准命令、管道、环境变量、函数、控制结构等。用户可以使用Bash来执行各种任务,如文件管理、进程控制、网络通信、系统管理等。通过编写脚本,还可以实现自动化部署、日志处理、数据备份等复杂的功能。
6.3 Hello的fork进程创建过程
- hello程序开始运行,执行main函数中的代码。
- 进入fork()函数,系统会为当前进程创建一个新的子进程。
- 子进程从fork()函数处继续执行,而父进程也从fork()函数处继续执行,但是两个进程现在是互相独立的。
- 在子进程中,执行Hello的代码。
- 子进程结束,返回0。
- 在父进程中,等待子进程结束,然后返回子进程的状态值。
- 父进程结束,程序运行结束。
6.4 Hello的execve过程
Shell调用execve函数后,加载器在程序头表的引导下,将可执行目标文件hello的片复制到代码段和数据段。如图为内存映像。
接着加载器跳至程序的入口点,即_start, _start调用系统,启动_lib_start_main
Lib_start_main初始化执行环境,调用用户层的main函数,处理main的返回值。
6.5 Hello的进程执行
(1) 开始时,Shell会创建hello的进程,并将其运行在用户态。
(2) 在程序执行过程中,根据需要,内核会进行上下文切换并调度不同的进程执行。例如,当hello调用sleep函数进行等待时,内核可能会选择调度其他进程以充分利用系统资源。但是,由于内核具有良好的上下文保存机制,所以即使hello被暂停执行,当内核重新调度hello继续执行时,它也可以完好地继续运行,似乎没有被打断一样。
(3) 在上下文切换期间,程序处于内核模式,而当内核完成切换后,则会返回到用户模式以继续执行程序。执行过程如下图
6.6 hello的异常与信号处理
6.6.1 异常
hello程序在执行过程中可能会出现以下几类异常:
类别 | 原因 | 异步/同步 | 返回行为 |
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
6.6.2 信号
常见的信号种类如下
ID | 名称 | 默认行为 | 相应事件 |
2 | SIGINT | 终止 | 来自键盘的中断 |
9 | SIGKILL | 终止 | 杀死程序 |
11 | SIGSEGV | 终止 | 无效的内存引用(段故障) |
14 | SIGALRM | 终止 | 来自alarm函数的定时器信号 |
17 | SIGCHLD | 忽略 | 一个子进程停止或者终止 |
6.6.3 各命令及运行截屏
(1)不停乱按
分析:乱按并未对hello程序本身造成任何影响。这是因为Shell只有一个前台任务,所有的输入都会停留在缓冲区中,等hello结束后才会进行处理
(2)CTRL-C
分析:使用CTRL-C会导致内核发送SIGINT信号至Shell的前台进程组,使得hello被终止。
(3)CTRL-Z
分析:CTRL-Z会导致内核发送一个SIGTSTP信号至Shell的前台进程组,使得hello被终止。
(4)Ctrl-z后运行ps jobs pstree fg kill 等命令
分析:当使用 Ctrl-Z 命令暂停 hello 进程后,它将进入后台。根据 ps 和 jobs 命令提供的信息,我们可以知道该进程并没有被终止或杀死。使用 fg 命令可以将其重新移回前台并恢复执行。再次使用 Ctrl-Z 命令使其暂停后,我们可以使用 kill 命令将其终止。最后,通过 jobs 命令查询该进程的状态,我们可以发现它已经被终止。
6.7本章小结
本章介绍了进程的概念与作用,简述了Shell-bash的作用和处理流程。说明了hello的fork进程创建过程和execve过程。简述了hello的进程执行过程,以及hello对异常和信号的处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
- 逻辑地址:程序在编写时使用的地址称为逻辑地址。它通常是以符号形式存在于程序中,具有可读性和易维护性。逻辑地址可以没有实际的物理地址对应。在这里指的是hello.o中的内容。
- 线性地址:在程序运行时,操作系统会将逻辑地址转换成线性地址。线性地址是一个虚拟地址,在内存中没有实际的存储位置,因此也无法被访问。但是,它是一种连续的地址空间,方便进行地址管理和分配。程序hello的代码会产生段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
- 虚拟地址:为了更好地管理和保护内存空间,操作系统将线性地址转换成虚拟地址。虚拟地址是一个与物理地址不同的地址空间,它将内存地址空间划分为多个虚拟页,并进行映射。不同的虚拟页可以映射到相同或不同的物理页,从而实现地址隔离和保护。程序hello运行在虚拟地址空间中。
- 物理地址:在执行程序时,CPU 需要将虚拟地址转换成实际物理地址,以便在内存中读取或写入数据。物理地址是内存的真实地址,对应着物理内存中的具体位置。通过地址映射等技术,操作系统和硬件可以实现虚拟地址到物理地址的转换。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel 处理器的架构中,逻辑地址到线性地址的转换使用了段式管理机制。具体的转换过程如下:
- 首先,CPU 会根据逻辑地址中的段选择器(Segment Selector)从全局描述符表(Global Descriptor Table,GDT)或本地描述符表(Local Descriptor Table,LDT)中读取对应的段描述符,以获取该段的基地址(Base Address)和大小(Limit)等信息。
- 接着,CPU 将逻辑地址中的偏移量(Offset)与段描述符中的基地址进行相加,得到线性地址。
- CPU 进一步将线性地址拆分成两部分:高位部分为段选择器(Segment Selector),低位部分为段内偏移量(Offset),并按照段描述符中的权限和大小等限制进行检查。
- 在这个过程中,由于 Intel 处理器支持多级保护模式(Multilevel Protection Mode),因此还需考虑特权级的影响。如果当前执行的代码的特权级(Privilege Level)较低,则需要确保所访问的段具有足够的权限和保护级别。
7.3 Hello的线性地址到物理地址的变换-页式管理
- 首先,操作系统根据 CPU 提供的页目录表基地址(Page Directory Table Base Address)和页目录项索引(Page Directory Entry Index),从页目录表中读取相应的页目录项(Page Directory Entry)。
- 页目录项中存储着一个指向页表的物理地址,因此操作系统需要将该物理地址转换成内存中的页表地址,从而得到页表中的页表项(Page Table Entry)。
- 由于 Hello 程序只有一个代码段,因此线性地址所在的页表项应该是代码段对应的页表项。在该页表项中,存储着一个指向物理内存中实际地址的页帧号(Page Frame Number),并且还包含一些页面权限和状态等信息。
- 最终,操作系统将页表项中的偏移量与线性地址的低 12 位进行相加,得到最终的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB(Translation Lookaside Buffer)是 CPU 中的一种高速缓存,用于加速虚拟地址到物理地址的转换。在使用四级页表时,VA(虚拟地址)需要通过多级页表才能映射成 PA(物理地址),这样就会降低地址转换的速度。而 TLB 的作用就是在进行地址转换时,缓存常用的映射关系,从而提高地址转换的速度。
下面是 TLB 与四级页表支持下的 VA 到 PA 的变换过程:
- 首先,CPU 将虚拟地址分成两个部分:高位部分是页表索引(Page Table Index),用于访问对应的页表,低位部分是页内偏移量(Page Offset)。
- CPU 尝试从 TLB 中查找是否存在该虚拟地址到物理地址的映射关系。如果 TLB 中没有找到对应的映射,则会触发一个 TLB Miss 异常,CPU 需要通过读取页表来完成地址转换。
- 如果 VA 在 TLB 中已有映射,则可以直接读取对应的 PA,否则需要访问页表来获取相应的物理地址。在四级页表中,CPU 需要根据虚拟地址的页表索引依次访问四级页表中的不同层次(每层包含 512 个页表项)以获取物理地址的页框号(Page Frame Number),并将其与 VA 中的页内偏移量组合成最终的 PA。
- 如果页表中所需的页表项已经缓存在 TLB 中,则 CPU 可以快速地完成 VA 到 PA 的转换。否则,需要先从内存中读取页表项,并更新 TLB 中相应的映射关系。
总之,TLB 和四级页表的组合可以提高虚拟地址到物理地址的转换速度和效率,从而更好地支持操作系统的内存管理功能。
7.5 三级Cache支持下的物理内存访问
- 当CPU需要访问某个地址时,首先会查询一级缓存(L1 Cache),如果数据在其中,则直接调用该数据并返回给CPU。
- 如果数据不在一级缓存中,那么CPU就会去查找二级缓存(L2 Cache),如果数据在其中,则从二级缓存获取数据并返回给CPU。
- 如果数据既不在一级缓存也不在二级缓存中,那么CPU就会去查找三级缓存(L3 Cache)。如果数据在其中,则从三级缓存获取数据,并将数据复制到二级缓存和一级缓存中。从此以后,CPU的后续访问就可以直接从缓存中获取数据了。
- 如果数据在三级缓存中也不存在,那么CPU就会从主存中读取数据。当数据从主存中加载到三级缓存中,CPU会将它缓存到三级缓存中,并根据需要把它同步到较高层次的缓存中。
- 当需要写入到内存时,CPU会首先将数据写入到一级缓存中,然后同步到二级缓存和三级缓存中,并最终写回主存。
7.6 hello进程fork时的内存映射
具体来说,在hello程序中,当执行fork时,父进程和子进程各自维护独立的虚拟地址空间,但实际上它们共享相同的物理内存页,也就是共享代码段和数据段。当子进程修改这些共享页的内容时,操作系统会为其复制一份新的物理页,使之成为子进程独立的副本,从而避免了父子进程在修改同一块内存时发生冲突。
7.7 hello进程execve时的内存映射
在hello中,在execve调用成功后,操作系统会将新程序的代码和数据段加载到进程的虚拟内存地址空间中,并重新计算每一个虚拟地址对应的物理地址。同时,操作系统也会结束进程当前运行时所使用的所有资源,例如打开的文件描述符、信号处理程序等。
因此,在执行execve调用时,原先的内存映射将被完全替换为新的程序的内存映射,所有的内存页都将被重新映射。这意味着,原来在进程中分配的所有动态内存都将被释放,并且需要重新分配新的内存。此外,由于新程序的代码段和数据段不同于原先的程序,从而导致新程序的内存占用情况也有所变化。
7.8 缺页故障与缺页中断处理
缺页故障和缺页中断处理的主要流程如下:
- 当进程访问一个不在内存中的虚拟页时,CPU会检测到这个错误,并通过中断机制向操作系统发出中断请求。
- 操作系统的中断处理程序会记录发生的缺页事件,并判断该页是否已经在内存中。如果该页已经在内存中,则说明中断是由于软件错误或其他类型的硬件错误引起的,此时操作系统会进行相应的错误处理。
- 如果该页在物理内存中不存在,则说明产生了缺页故障,此时操作系统会启动缺页中断处理程序。该程序会进行如下处理:
a. 操作系统会检查是否有空闲的物理页面用于加载缺失的页面,如果没有,则需要使用页面置换算法从内存中选择一个牺牲页(victim page)并将其写回到磁盘中,以腾出一个空闲的物理页面。
b. 操作系统会从磁盘中读取该页,并将其加载到空闲的物理页面中。此时,操作系统会更新页表和其他相关数据结构以反映该页面已经在内存中。
c. 当缺失页面被加载到物理内存中后,CPU会重新执行引起缺页故障的指令,并将控制权返回给进程,使其能够继续执行。
- 如果操作系统无法将缺失的页面加载到内存中(例如,由于磁盘故障),则会通知进程相应的错误信息并终止运行。
7.9动态存储分配管理
在上述hello程序中,使用了动态存储分配,即通过调用malloc函数从堆中申请一块指定大小的内存空间,并将其地址赋值给指针变量。
动态存储分配是一种灵活、高效的内存管理方式,可以让程序在运行时请求内存,而不需要提前分配固定大小的内存空间,从而避免了内存浪费和运行时内存不足的风险。
当我们使用printf输出较长的字符串时,printf会调用malloc来动态分配内存空间以存储该字符串。以下是动态内存管理的基本方法与策略:
- 内存分配方式:动态内存管理最基本的方法就是动态内存分配。常见的内存分配函数有malloc、calloc、realloc等。其中,malloc可以从堆中动态分配一段指定大小的内存空间,并返回该空间的起始地址;calloc则可以分配一段连续空间并初始化为0;realloc则可以重新调整已分配内存空间的大小。printf内部会调用malloc动态分配内存空间存储字符串。
- 内存释放方式:动态内存分配所得到的内存空间只有在使用完毕后被主动释放才能够进行再利用。使用free函数可以将之前使用malloc、calloc、realloc等函数分配的内存块释放回堆中。在printf中,该内存空间在输出完毕后,由操作系统自动完成释放。
- 内存保护机制:在动态内存分配时,通常也需要考虑内存保护的问题。由于程序存在逻辑错误或非法操作,可能会引发指针越界访问、内存覆盖、重复释放等问题。因此可以使用一些内存保护机制,如内存检查器、内存边界标记、canary技术等,以保护程序不易受到这些问题的影响。操作系统或编译器一般会自动完成这些内存保护工作。
- 算法和策略:在分配内存空间时,在堆的结构、块的大小、可用性等方面进行算法设计和策略制定,以达到高效利用内存资源。常见的有分配器算法、内存池技术、伙伴系统等。在printf的内部实现中,也会有特定的算法实现和内存管理策略。
7.10本章小结
本章主要介绍了HELLO操作系统中的存储管理相关内容。首先介绍了HELLO的存储器地址空间,包括各个区域的作用和划分。然后介绍了INTEL逻辑地址到线性地址的变换方式,即段式管理,以及线性地址到物理地址的变换方式,即页式管理。接着介绍了TLB与四级页表支持下的虚拟地址到物理地址的变换过程。随后介绍了三级CACHE支持下的物理内存访问。然后分别讨论了HELLO进程FORK时的内存映射和HELLO进程EXECVE时的内存映射。最后介绍了缺页故障与缺页中断处理,以及动态存储分配管理,主要包括内存分配方式、内存释放方式、内存保护机制、算法和策略等方面的内容。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux的IO设备管理主要涉及以下几个方面:
- 设备驱动程序:设备驱动程序是Linux系统中使用最广泛的设备管理方式,它是一种软件模块,负责将设备的硬件接口与操作系统之间的软件接口进行对接。在Linux系统中,设备驱动程序分为两类:字符设备驱动和块设备驱动。
- 文件系统:Linux系统中的所有设备都被看作是文件,通过文件系统对其进行统一管理。文件系统不仅管理着硬盘中的存储设备,还包括网络设备、USB设备等。
- 虚拟文件系统:虚拟文件系统是Linux系统中的一个重要组成部分,它用于管理所有文件系统的挂载点,以及维护内核数据结构和缓存机制。虚拟文件系统可以实现不同文件系统间的无缝切换,并提供了统一的IO操作接口,使得应用程序可以向任何文件系统请求数据。
- IO调度器:IO调度器是Linux内核中的一个核心模块,负责管理磁盘读写请求的顺序和优先级,以提高磁盘性能。Linux系统中常用的IO调度算法有CFQ、NOOP、Deadline等。
- 内存映射I/O:内存映射I/O是Linux系统中的另一种设备管理方式,它通过将磁盘上的数据直接映射到内存中,使得应用程序可以通过内存空间和磁盘数据直接交互。这种方式可以提高文件读写的效率和速度。
8.2 简述Unix IO接口及其函数
Unix IO接口指的是Unix操作系统提供的一组标准输入输出函数,用于实现与文件、设备、管道等IO资源的交互。这些函数主要包括以下几个方面:
- 打开和关闭函数:打开函数用于打开一个文件、设备或者管道,而关闭函数则用于关闭一个已经打开的文件、设备或者管道。常见的打开函数包括open()、creat()等,而关闭函数则是close()函数。
- 读写函数:读写函数用于从文件、设备或者管道中读取数据或将数据写入到它们当中。常见的读函数包括read()、pread()等,常见的写函数包括write()、pwrite() 等。
- 文件描述符函数:文件描述符函数用于创建、复制和操作文件描述符(file descriptor)。常见的文件描述符函数包括dup()、pipe() 等函数。
- 状态查询函数:状态查询函数用于查询已经打开的文件、设备或者管道的状态信息。常见的状态查询函数包括fstat()、lstat()等函数。
- 控制函数:控制函数用于掌控文件、设备或者管道的操作方式。常见的控制函数包括ioctl() 等函数。
除了上述函数外,还有一些其他的函数,如file操作函数、mmap(memory map,内存映射)函数、select()和poll()函数等。
8.3 printf的实现分析
在Linux系统中,printf函数的实现一般依赖于标准C库(libc),而 write函数是由Linux内核提供的系统调用之一。具体实现过程如下:
- printf函数调用:当用户程序调用printf函数时,首先进入标准C库,该库会将要输出的内容格式化为一个字符串,并返回给用户程序。
- write系统调用:用户程序得到printf函数返回的字符串后,使用write系统调用将其写入标准输出缓存区中。write函数的定义如下:
ssize_t write(int fd, const void *buf, size_t count);
fd指定输出的文件句柄,buf表示要输出的内容,count表示需要输出的字节数。
- 陷阱-系统调用机制:write函数的实际执行是通过触发系统调用来完成的。在Linux系统中,这个过程涉及到的多是硬件和操作系统内核之间的交互,包括CPU模式的切换和内存映射等。具体而言,在x86架构下,触发系统调用的方法通常是使用int 0x80汇编指令,将控制权传递到操作系统内核中。当内核捕获到该中断信号时,程序计数器指向操作系统内核程序,引导用户态程序进入内核态。内核程序获取参数并执行相应的系统调用处理函数,如对应write函数的_syscall6函数,将要写入的数据从用户空间拷贝到内核空间,最终写入到设备文件中。
SYSCALL_DEFINE6(write, unsigned int, fd, const char __user *, buf, size_t, count, unsigned long, pos) { struct fd f = fdget_pos(fd, &pos); ssize_t ret = -EBADF; if (f.file) { loff_t pos = f.file->f_pos; if (!(f.file->f_mode & FMODE_READ)) { char *from = (char __user *)buf; size_t len = count; unsigned long limit = current->mm->end_code; if (unlikely(limit - fs_base((unsigned long)from) < len)) len = limit - fs_base((unsigned long)from); ret = kernel_write(f.file, from, len, &pos); if (ret > 0) add_rchar(current, ret); } fdput_pos(f); } return ret; } |
- 字符显示驱动子程序:在Linux内核中,字符显示驱动子程序实现了将每个字符转化为对应的字模库,再转化成存储每个像素点RGB颜色信息的vram格式,并向显示设备输出的功能。这个过程具体实现由Linux内核的VIdeo for Linux 2(V4L2)框架来完成。V4L2是一个视频设备的抽象层,它为用户程序提供了一个通用的接口,允许用户程序通过设备节点文件(/dev/video0等)向各种类型的视频设备进行读写操作。
- 刷新屏幕:对于液晶显示器等设备,Linux内核中有相应的驱动程序,通过该程序刷新屏幕。具体而言,当显卡芯片将vram的RGB分量输出到数据端口时,液晶显示器会将信号转换为亮度和色彩信号,并输出给人眼观察。显卡芯片根据预设的屏幕分辨率,逐行读取vram,并将RGB分量输出到信号线,完成对液晶显示器的控制。
8.4 getchar的实现分析
在Linux系统下,getchar函数是用于从标准输入流中获取一个字符的函数。这个函数的实现借助了read系统调用,通过读取文件描述符(stdin)的输入缓冲区来获取用户输入的字符。以下是对getchar的实现进行分析,包括异步异常-键盘中断的处理。
当用户输入字符时,会触发系统中的硬件中断,并将扫描码存储在键盘控制器中。而在Linux系统中,硬件中断被提前同步到内核空间,并由内核的键盘中断处理程序进行处理,具体的处理过程如下:
- 键盘控制器会向CPU发送一个中断请求信号,使CPU停止当前正在执行的程序,并切换到中断处理程序。
- 内核中的键盘中断处理程序会从键盘控制器中读取扫描码。
- 通过查询键码表,将扫描码转换成对应的ASCII码。
- 将转换后的ASCII码保存到系统的键盘缓冲区中。
- 通知等待用户输入的进程有新的输入可用(如果它们已经阻塞在read系统调用上)。
接下来,当用户调用getchar时,会执行以下操作:
- 调用read系统调用以读取stdin文件描述符的输入缓冲区中的数据。
- 如果stdin文件描述符中没有数据或者数据不足,read系统调用会阻塞并等待数据的到来。
- 如果读取到了数据,就返回该数据中的第一个字符。
- 如果读取到回车键(即ASCII码为'\n'),则停止读取,并将读取到的数据返回给调用者。
8.5本章小结
本章主要讲解了 Linux 系统下的 IO 管理,分为四个部分:
第一部分介绍了 Linux 的 IO 设备管理方法,包括了设备文件的创建、多路复用机制、异步 IO 等内容。通过对 IO 设备进行管理,可以更好地利用系统资源,并提高程序效率。
第二部分简述了 UNIX IO 接口及其函数,包括文件描述符、IO 系统调用和标准 IO 库等,这些接口和函数为读写文件提供了便利。同时,由于 UNIX 的 IO 接口在不同的操作系统中都有实现,因此学习这些接口也可以帮助我们更好地理解其他操作系统下的 IO 系统。
第三部分对 printf 函数的实现进行了分析,主要从格式化字符串解析、参数传递和输出等方面进行了介绍。printf 函数是 C 语言中常用的输出函数之一,它能够将格式化的数据输出到标准输出流或文件中。
第四部分对 getchar 函数的实现进行了分析,主要从键盘中断处理、read 系统调用等角度进行了讲解。getchar 函数是 C 语言中常用的输入函数之一,通过它我们可以从标准输入流中读取用户输入的数据。
结论
- 了解计算机系统的基本组成和运行原理,包括CPU、内存、IO等部分,并理解它们之间的交互工作方式。
- 掌握Linux操作系统下的C语言编程技巧,如内存操作、文件读写等相关操作。
- 理解HELLO程序的架构设计和执行流程,包括预处理、编译、汇编、链接、进程管理、存储管理和IO管理等过程,并认真分析每个阶段的实现细节。
- 总结HELLO程序所经历的过程并反思,提出新的设计与实现方法,例如面向对象的设计思想、硬件与软件的协同工作、实验及测试、全局优化等方面,并将其融入到系统的设计中去。
附件
中间产物的文件名 | 说明 |
hello.i | 由hello.c经预处理生成 |
hello.s | 由hello.c经编译生成(带-m64等选项) |
hello.o | 由hello.s经汇编生成 |
hello_o_elf.txt | hello.o的readelf输出结果 |
hello_o_dis.txt | hello.o的反汇编输出结果 |
hello | hello.o与其他文件经链接生成的文件 |
hello_elf.txt | hello的readelf输出结果 |
hello_dis.txt | hello的反汇编输出结果 |
参考文献
- The C Preprocessor: Preprocessor Output
- 编译过程的六个阶段_请叫我Oscar的博客-CSDN博客
- 深入了解运行时栈(C语言)_运行栈_头疼的太阳花的博客-CSDN博客
[4] https://www.runoob.com/w3cnote/gcc-parameter-detail.html
[5] https://www.cnblogs.com/skywang12345/archive/2013/05/30/3106570.html