HIT CSAPP 期末大作业

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业  计算机科学与技术                      

学     号                        

班   级                        

学       生                   

指 导 教 师                      

计算机科学与技术学院

20234

摘  要

一个看似简单的程序hello,实际上在计算机中经历了复杂的生命周期。本文将跟踪hello的生命周期,剖析其从预处理、编译、汇编、链接生成可执行文件到在系统上运行,最后运行完毕被回收的过程。这个过程将揭示计算机系统的奇妙之处,借此加深对计算机系统的理解。

关键词:计算机系统;hello可执行程序;编译;汇编;链接;进程;存储;IO管理                            

目  录

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

P2P过程

P2P指的是从程序(Program)转变为进程(Process)的过程。在这个过程中,Hello程序被输入到计算机中,经过预处理、编译、汇编和链接等步骤,最终生成可执行文件。

具体过程:

  1. 预处理:将程序文件经过预处理器处理,进行宏展开、头文件包含等操作,并生成预处理后的代码文件。
  2. 编译:预处理后的代码文件被编译器处理,将高级语言代码转换为汇编语言代码。
  3. 汇编:汇编器将汇编语言代码转换为机器指令,生成可执行文件的二进制代码。
  4. 链接:链接器将可执行文件的二进制代码与所需的库文件进行连接,生成最终的可执行文件。
  5. 进程:最终的可执行文件被操作系统加载并创建为一个进程,可以在计算机系统中独立运行。

020过程

020是指Hello程序的整个执行过程。在执行过程中,Hello程序首先被操作系统的壳(Bash)调用,操作系统负责进程管理。操作系统通过fork系统调用创建一个新的进程来运行Hello程序,然后使用execve系统调用将Hello程序加载到内存中。同时,操作系统还负责内存的管理,包括虚拟地址(VA)到物理地址(PA)的转换,以及使用MMU、TLB、多级页表和缓存等技术来加速内存访问。操作系统还管理CPU、内存和输入输出设备(如键盘、主板、显卡和屏幕)的资源分配和调度,使Hello程序能够在计算机硬件上顺利运行。

在Hello的表演过程中,操作系统为Hello进程分配时间片,使其能够在CPU上执行指令,包括取指、译码和执行等步骤。同时,操作系统与MMU协作,为Hello进程提供虚拟内存到物理内存的映射,以及使用TLB、多级页表和缓存等技术来加速内存访问。操作系统还负责处理IO操作和信号处理,使Hello程序能够与键盘、主板、显卡和屏幕等设备进行交互。

整个过程中,操作系统与计算机系统的各个组件(编辑器、编译器、汇编器、链接器、操作系统、CPU、内存和IO设备等)紧密合作,共同完成Hello程序的执行。虽然Hello程序的表演时间很短暂,演技看起来不怎么样,但它代表了计算机系统的伟大和复杂性。在整个历史长河中,不断有菜鸟程序员与Hello擦肩而过,只有懂得计算机系统的大佬们才能真正理解Hello的生、死和坎坷。

1.2 环境与工具

硬件环境:

主机:

虚拟机:

软件环境:

主机:

Windows11 64位操作系统

虚拟机:

Ubuntu 22.04.3 LTS(64位)

开发与调试工具:

gedit、gcc、gdb、readelf、objdump、edb

1.3 中间结果

文件名

功能

hello.c

c语言源程序

hello.i

预处理生成的文本文件

hello.s

编译器生成的汇编语言文本文件

hello.o

汇编器生成的二进制可重定位目标文件

hello

链接生成的二进制可执行目标文件

1.4 本章小结

本章叙述了hello程序的P2P和020过程,介绍了编写大作业用到的实验环境及工具,列出了实验过程中的中间结果文件。


第2章 预处理

2.1 预处理的概念与作用

概念:

预处理是C语言的一个重要功能,它由预处理程序负责完成。当编译一个程序时,系统将自动调用预处理程序对程序中的“#”号开头的预处理部分进行处理,处理完毕之后可以进入源程序的编译阶段。C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等。

作用:

  1. 宏展开:预处理器可以识别源代码中的宏定义,并将其展开为相应的代码片段。宏展开可以实现代码复用,提高代码的可读性和可维护性。
  2. 头文件包含:预处理器可以解析源代码中的#include指令,将指定的头文件内容插入到源代码中。头文件包含可以实现代码的模块化和复用。
  3. 条件编译:预处理器可以根据条件编译指令(如#ifdef、#ifndef、#if、#elif、#else和#endif)来选择性地编译或排除某些代码块。条件编译可以根据不同的编译选项或平台,实现不同的代码逻辑。
  4. 常量定义:预处理器可以使用#define指令定义常量,将源代码中的符号替换为对应的常量值。常量定义可以提高代码的可读性和可维护性,同时也可以避免魔法数值的出现。
  5. 注释删除:预处理器可以删除源代码中的注释,将注释行转换为空白行或删除。注释删除可以减小编译后的目标文件大小,提高编译速度。

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

查看hello.i文件,发现hello.c中的注释和编译预处理指令消失,main函数位于文件的最后,前面插入了所有指定头文件的内容。

 2.4 本章小结

本章简述了预处理的概念和作用,并在untunbu下实现了hello.c的预处理,对预处理的结果进行了分析。


第3章 编译

3.1 编译的概念与作用

概念:

编译是指将高级语言源代码转换为机器可执行代码的过程。在这个过程中,编译器会经历多个阶段,包括预处理、词法分析、语法分析、语义分析、中间代码生成、优化和目标代码生成等。在预处理阶段,源代码经过宏展开、头文件包含等处理操作生成预处理后的代码(.i文件)。而从预处理后的文件生成汇编语言程序(.s文件)是编译的一部分。        

作用:

  1. 语法检查和错误检测:编译器会对源代码进行语法分析和语义分析,检查代码是否符合语法规则和语义约束,同时检测并报告代码中的错误。这有助于提前发现和修复代码中的问题,提高代码的质量和可靠性。
  2. 优化:编译器会对中间代码进行优化,以提高程序的执行效率和性能。优化可以通过消除冗余代码、减少内存访问、简化表达式等方式来改进程序的执行速度和资源利用。
  3. 目标代码生成:编译器会将中间代码转换为机器可执行的目标代码(汇编语言),将高级语言的抽象表示转化为底层机器指令,从而能够在目标平台上直接执行。
  4. 跨平台开发:编译器可以将高级语言代码编译为不同平台的目标代码,实现跨平台开发。通过编译器的工作,开发人员可以在不同的操作系统和硬件平台上运行同一份源代码,提高开发效率和代码的可复用性。

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1数据

(1)字符串常量

定义了标签.LC0、.LC1,并分别将一个字符串常量存储在标签对应的内存位置上。前面的.section .rodata表示只读数据区,.alion 8表示对齐方式为8字节对齐。

观察易发现存储的字符串常量为源程序中的以下部分:

.LC0是一个字符串常量的标签,(%rip)表示相对于指令指针(RIP)的偏移量。因此,.LC0(%rip)获取了字符串常量.LC0的地址。leaq .LC0(%rip), %rax的作用是将字符串常量.LC0的地址加载到寄存器%rax中。同理,leaq .LC1(%rip), %rax的作用是将字符串常量.LC1的地址加载到寄存器%rax中。

(2)局部变量

  1. argc和argv

subq$32,%rsp为函数的栈帧分配了32字节的空间,局部变量、函数参数、返回地址等信息将会存储在这片区域中。将寄存器%edi中的值(即argc的值)存储在堆栈帧的偏移量为-20(%rbp)的位置,将寄存器%rsi中的值(即argv的地址)存储在堆栈帧的偏移量为-32(%rbp)的位置。argc和argv的初值是由调用main函数时传递的实际参数决定的。根据源代码可以得知argc被定义为int类型,在64位环境中占4个字节,所以用movl,64位环境中地址占8个字节,所以用movq。

  1. i

源程序中定义了整型局部变量i但没有初始化。movl $0, -4(%rbp)的将立即数0赋值给位于-4(%rbp)地址处的32位(4字节)变量,即赋值给i。

后序执行过程中也有大量的mov,lea等赋值操作,再此不赘述。

(3)有符号整型常数

源程序中出现的这类数据用立即数($+数字)表示。

3.3.2算术操作

如上图为汇编代码中出现的算术运算,其中addl $1, -4(%rbp) 的作用是将立即数1加到相对于栈帧指针(%rbp)的偏移量为-4的内存地址中存储的值,对应源程序中的i++

subq$32,%rsp的作用前面已经说过,为函数的栈帧分配了32字节的空间。

movq -32(%rbp), %rax将argv[0]的地址存入%rax,因此addq $16, %rax获取argv数组中的第三个元素(即argv[2])的地址。同理,addq $8, %rax获取argv[1]的地址,addq $24, %rax获取argv[3]的地址。

3.3.3逻辑/位操作

源程序中没有出现逻辑/位操作。

3.3.4关系操作

-20(%rbp)处的值,即argc和4比较,若相等,则跳转到.L2,执行for循环,否则顺序执行if范围内的操作。-4(%rbp)处的值,即i和7比较,若i<=7(对应源代码中的i<8),则跳转到.L4,执行for循环语句内的操作,否则退出循环。

3.3.5数组/指针/结构操作

源程序中涉及到了对数组的操作。

前面已经分析过addq $16, %rax获取argv[2]的地址,addq $8, %rax获取argv[1]的地址,addq $24, %rax获取argv[3]的地址。由此可知movq (%rax), %rdx的作用是将argv[2]的值存入寄存器%rdx。同理,movq (%rax), %rax将argv[1]的值存入%rsi,movq %rax, %rdi将argv[3]的值存入%rdi。

3.3.6控制转移

源程序中出现的控制转移为if语句和for循环语句,对应到汇编代码如上图所示。可以看出汇编代码中的控制转移由关系操作和条件跳转指令结合实现。

3.3.7函数操作

源程序中进行了5次函数调用,在汇编代码中的对应指令如图所示。

函数调用时,前六个参数保存在寄存器中,其余的参数保存在栈中(此处没有用到),栈顶存放返回地址。main函数调用其他函数时,将被调用函数的第一条指令的地址写入程序指令寄存器%rip中,同时将返回地址压栈,执行完毕后,指令ret将返回地址从栈中弹出,写入程序指令寄存器%rip,函数返回,继续执行main函数的操作。

3.4 本章小结

本章简述了编译的概念和作用,并在untunbu环境下对hello.c进行编译。之后结合源程序,从数据、算术操作、关系操作、数组操作、控制转移、函数操作这些角度分析了汇编代码的实现方式。


第4章 汇编

4.1 汇编的概念与作用

概念:

汇编过程是将汇编语言代码转换为机器语言的过程。在这个过程中,汇编器(assembler)将汇编代码翻译成机器指令,并生成可执行的二进制文件。

作用:

  1. 可执行文件生成:汇编过程将汇编语言代码转换为机器语言的二进制程序,生成可执行文件或目标文件(object file)。这个文件可以直接在计算机上运行。
  2. 与硬件的交互:汇编过程将汇编语言代码转换为机器指令,这些指令直接与计算机硬件进行交互。通过汇编过程,程序员可以直接控制和操作计算机的底层硬件,如寄存器、内存等。
  3. 优化和调试:汇编过程可以进行代码优化,使得生成的机器代码更加高效。此外,通过查看和分析汇编代码,程序员可以进行程序的调试和性能优化。
  4. 与其他编程语言的结合:汇编过程常用于与高级编程语言(如C、C++)结合,通过汇编嵌入(inline assembly)或链接(linking)等方式,将汇编代码与其他编程语言的代码进行整合,以实现对底层硬件的直接控制和优化。

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

通过readelf -a hello.o > hello_o.elf生成hello.o的ELF格式文件。

下图为典型的ELF可重定向目标文件格式:

4.3.1

各个节对应的功能如下表:

功能

.text

已编译程序的机器代码

.rodata

只读数据

.data

已初始化的全局变量和局部静态变量

.bss

未初始化的全局变量和局部静态变量,仅是占位符,不占据任何实际磁盘空间

.symtab

符号表,存放函数和全局变量(符号表)信息,不包括局部变量

.rel.text

一个.tex节中位置的列表

.rel.data

被模块引用或定义的所有全局变量的重定位信息

.debug

一个调试符号表

.line

原始C源程序中的行号和.text节中机器指令之间的映射

.strtab

一个字符串表(包括.symtab和.debug节中的符号表)

4.3.2节头部表

读取ELF头:

ELF(Executable and Linkable Format)头是一种二进制文件格式的组成部分,用于描述可执行文件、可重定位文件和共享目标文件的结构和属性。ELF头位于二进制文件的起始位置,包含了文件的基本信息和元数据。ELF头是ELF格式的核心部分,它提供了关于文件的重要信息,以便操作系统和链接器能够正确加载、解析和执行文件。通过解析ELF头,操作系统可以了解文件的类型,确定入口点,并加载和执行文件的各个段和节。链接器可以使用ELF头的段表和节表信息来进行符号解析和重定位。

读取节头部表:

节头部表(Section Header Table)是ELF(Executable and Linkable Format)文件格式中的一个重要部分。它是位于ELF文件中的一个特殊节(称为.section headers)中的一个表,包含了关于各个节(sections)的信息和属性,如名称、大小、偏移量、对齐方式等。通过节头部表,解析程序可以了解ELF文件中各个节的属性和信息,从而正确加载、解析和执行文件。链接器可以使用节头部表的信息进行符号解析、重定位和链接。

读取可重定位节:

可重定位节(Relocatable Section)是在可重定位文件(Relocatable File)中存储数据或代码的一种数据结构。可重定位节是ELF(Executable and Linkable Format)文件格式中的一个重要组成部分。可重定位节包含了程序的各个部分,如代码段、数据段、符号表、字符串表等。每个可重定位节都有一个唯一的名称,用于标识和识别该节。通过可重定位节,链接器可以将不同的模块合并在一起,并根据重定位表和符号表进行符号解析和重定位。可重定位节在编译、链接和执行过程中起到了重要作用。

读取符号表:

符号表(Symbol Table)是在可执行文件、目标文件或共享库中存储符号信息的一种数据结构。符号表记录了程序中使用的函数、变量、类型等符号的名称、地址、大小和其他属性。符号表通常由编译器、汇编器和链接器生成和使用。编译器在编译过程中收集源代码中的符号信息,并将其记录在符号表中。链接器在链接过程中合并不同模块的符号表,并解决符号冲突和重定位。程序在运行时,可以通过符号表来查找和访问各个符号。

4.4 Hello.o的结果解析

反汇编代码:

汇编代码:

反汇编语言的构成和汇编语言的语法结构很相似,以下为二者的映射关系:

语法结构

映射关系

指令助记符

反汇编语言中的指令助记符与汇编语言中的指令助记符是一一对应的。例如,汇编语言中的"MOV"指令对应的反汇编语言中的"Mov"。

操作数

反汇编语言中的操作数通常是以符号形式表示,用于代表操作数的地址或值。操作数的具体表示形式取决于机器指令的特点和反汇编器的规则。例如,一个32位寄存器可能在汇编语言中表示为"EAX",在反汇编语言中可能表示为"eax"。

内存地址

在反汇编语言中,内存地址通常使用符号表示,例如"[eax]"表示eax寄存器中存储的内存地址。

标号

标号在反汇编语言中用于表示跳转目标的位置,它们可以与汇编语言中的标号相对应。反汇编器会根据机器语言指令中的跳转地址或偏移量来生成标号。

注释

注释在反汇编语言中用于提供对反汇编代码的解释和说明,帮助理解代码的功能和逻辑。注释与汇编语言中的注释相似。

二者的差异:

  1. 分支转移

反汇编代码

汇编代码

可以发现在汇编代码中,分支跳转是使用段名作为标识的,而在反汇编代码中,是用地址的偏移量来表示的。

  1. 函数调用

反汇编代码

汇编代码

可以发现,汇编代码中,函数调用的具体目的地是用函数名标识的,而在反汇编代码中是该条命令的下一条命令的地址。但是因为调用的函数还在其它库中,具体调用的地址无法确定,只有在经过链接后才能确定函数调用的准确位置。

4.5 本章小结

本章先介绍了汇编的概念和作用,对hello.s进行汇编,生成二进制可重定位目标文件hello.o,然后对hello.o的ELF格式进行分析,说明了机器语言和汇编语言的映射关系,以及二者在对分支转移和函数调用的处理上的差异。


第5章 链接

5.1 链接的概念与作用

概念:

链接的概念是将多个目标文件(Object File)合并为一个可执行文件的过程。在从 hello.o 到 hello 可执行文件的生成过程中,链接器会解析目标文件中的符号引用,将符号解析为实际的地址,并将代码和数据段合并到一个地址空间中,最终生成可执行文件。

作用:

  1. 符号解析和解决:链接器会解析目标文件中的符号引用,找到对应的符号定义,并将符号引用解决为实际的地址或位置。这样,当程序调用一个函数或引用一个变量时,链接器能够找到对应的定义,并将其替换为实际的地址。
  2. 地址重定位:由于每个目标文件可以独立编译,其中的代码和数据可能是相对于目标文件本身的地址。链接器负责将这些相对地址转换为最终的绝对地址,以确保程序在内存中正确地执行。链接器会对目标文件中的指令和数据进行地址重定位,使得程序能够在正确的内存位置执行。
  3. 目标文件合并:链接器将多个目标文件中的代码段、数据段和其他节(sections)合并到一个地址空间中,形成最终的可执行文件。通过合并目标文件,链接器将程序的各个模块组合在一起,使得程序能够作为一个整体进行执行。
  4. 库文件链接:链接器还负责将程序所需的库文件链接到可执行文件中。库文件中包含了一组已经编译好的函数和代码,可以供程序调用和使用。链接器会将程序中对库文件中函数的引用解析为对应的库函数地址,并将库文件中的代码合并到可执行文件中,以便程序能够调用库中提供的函数和服务。

5.2 在Ubuntu下链接的命令

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

    

ELF头

只读内存段(代码段)

段头部表

(将连续的文件节映射到运行时代码段)

.init

.text

.rodata

.data

读/写内存段(数据段)

.bss

.symtab

不加载到内存的符号表和调试信息

.debug

.line

.strtab

节头部表

(描述目标文件的节)

读取ELF头:

可执行程序文件的 ELF(Executable and Linkable Format)头是一种特殊的数据结构,位于可执行文件的开头。ELF 头包含了关于可执行文件的元信息,用于描述文件的结构和属性。

读取节头部表:

可执行程序文件的节头部表(Section Header Table)是一种数据结构,用于描述可执行文件中的各个节(section)的信息。每个节都是可执行文件中的一个区域,包含了不同类型的数据,如代码、数据、符号表、字符串表等。节头部表是一个由多个节头部表项(Section Header Entry)组成的数组,每个节头部表项对应一个节。每个节头部表项包含了关于该节的元信息,如节的名称、类型、大小、偏移量等。

读取可重定位节:

可执行程序文件中的可重定位节(Relocatable Section)是一种特殊类型的节,用于存储重定位信息。可重定位节包含了指令或数据的地址重定位所需的信息,用于在链接时进行符号解析和地址重定位。当可执行程序被加载到内存中时,这些重定位信息将被使用来修正指令和数据的地址,以确保程序能够正确地执行。

读取符号表:

可执行程序文件的符号表(Symbol Table)是一种数据结构,用于存储程序中定义和引用的符号(Symbol)信息。符号可以是变量、函数、类、结构体等标识符的名称,用于在程序中引用和定位特定的代码或数据。

5.4 hello的虚拟地址空间

    

Data Dump区域显示了虚拟地址空间的信息,可以看到hello程序从虚拟空间0x401000载入,查看Symbols发现是.init的地址,与5.3中节头部表中的地址一致。

其他节的地址也是一一对应的。.text节与虚拟地址空间中的_start函数有相同的地址,是程序的入口地址。根据.rodata节的地址,可以找到虚拟地址空间中对应地址存放的是两个连续的字符串。虚拟地址空间中没有找到.symtab、.debug、.line、.strtab的对应位置,因为它们不加载到内存。

5.5 链接的重定位过程分析

hello.o反汇编结果:

hello反汇编结果:

观察hello.o和hello的反汇编结果,主要有以下几个方面不同:

  1. hello.o只有对.text节反汇编的结果,并且只有main函数的指令,而hello有多个节的反汇编结果。
  2. hello.o中的地址是相对main函数的偏移地址,而hello中的地址是虚拟地址空间中的地址,main函数中的标签变成了实际的地址。
  3. hello.o中函数调用用当前地址的下一个地址占位,而hello是跳转到函数的地址。
  4. hello.o对只读数据的访问进行占位标记,而hello中有具体的偏移量。

出现以上结果是因为hello.o没有经过链接,main的地址从0开始,并且不存在库函数的代码。而链接后的hello重定位得到虚拟空间中的绝对地址,并添加了库函数。

重定位的一般过程:

  1. 符号解析:链接器首先会收集所有目标文件中的符号引用和符号定义,包括函数、变量和其他符号。符号引用是指在目标文件中使用的符号,而符号定义是指在目标文件中定义的符号。
  2. 符号表生成:链接器会根据符号解析的结果生成一个符号表,其中记录了每个符号的名称、类型和位置等信息。符号表会在后续的重定位过程中使用。
  3. 地址计算:链接器根据目标文件中的符号引用和符号定义的位置,计算每个符号引用相对于符号定义的地址偏移量。这个地址偏移量会在后续的重定位步骤中用于修正符号引用的位置。
  4. 重定位:链接器会遍历目标文件中的每个需要重定位的位置,将符号引用替换为相应的符号定义的地址。根据具体的重定位方式,链接器会使用不同的修正方法,如绝对地址重定位、相对地址重定位等。
  5. 符号重定位:链接器会将符号表中的符号引用替换为符号定义的实际地址。这样,当程序执行时,可以正确地访问和使用这些符号。
  6. 可执行文件生成:链接器完成重定位后,将目标文件中的代码、数据和重定位信息组合在一起,生成最终的可执行文件。这个可执行文件包含了所有目标文件的内容,并且可以直接运行。

以exit函数为例说明重定位的过程。

hello.o:

hello:

可以看出未重定位前call命令的地址码为0占位。hello中call命令的地址是0x401203,执行call指令时,PC的值为下一条指令的地址0x401208,而exit函数地址为0x4010d0,两者之差为0xfffffec8e8,因为Linux里用的是小端法,因此call指令的地址码为e8c8feffff。

    1. hello的执行流程

子程序名

地址

_init

0x0000000000401000

.plt

0x0000000000401020

puts@plt

0x0000000000401090

printf@plt

0x00000000004010a0

getchar@plt

0x00000000004010b0

atoi@plt

0x00000000004010c0

exit@plt

0x00000000004010d0

sleep@plt

0x00000000004010e0

_start

0x00000000004010f0

_dl_relocate_static_pie

0x0000000000401120

deregister_tm_clones

0x0000000000401130

register_tm_clones

0x0000000000401160

_do_global_dtors_aux

0x00000000004011a0

frame_dummy

0x00000000004011d0

main

0x00000000004011d6

_fini

0x0000000000401270

    1. Hello的动态链接分析

延迟绑定是通过GOT(Global Offset Table)和PLT(Procedure Linkage Table)实现的。GOT是数据段的一部分,而PLT是代码段的一部分。下面是两个表的详细内容:

PLT:PLT是一个数组,其中每个条目是16字节的代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有自己的PLT条目。每个条目负责调用一个具体的函数。

GOT:GOT是一个数组,其中每个条目是8字节的地址。当与PLT结合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时所需的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个与之对应的PLT条目。

在之前的节头部表中找到got的地址:

   

在edb的data dump中找到对应位置:

发现运行dl_int后的信息发生了变化。

5.8 本章小结

本章介绍了链接的概念与作用,在untunbu下将hello.o链接成hello,然后生成hello的ELF格式文件,并对其进行了分析。接着利用edb查看了hello的虚拟地址空间,与头部表进行了对照。最后分析了链接的重定位过程,展示了hello的执行流程,对hello的动态链接进行了分析。


6hello进程管理

6.1 进程的概念与作用

概念:

进程是计算机中正在运行的程序的实例。它是操作系统分配和管理资源的基本单位,用于执行各种任务和操作。每个进程都有自己的内存空间、代码、数据和系统资源。进程可以包含一个或多个线程,每个线程都是进程中执行的独立单元。进程之间是相互隔离的,每个进程都在自己的地址空间中运行,互不干扰。

作用:

  1. 并发执行:操作系统可以同时运行多个进程,使得多个任务能够并发执行,提高系统的效率和响应能力。
  2. 资源分配:每个进程都有自己的资源需求,例如内存、CPU时间、文件和设备等。操作系统负责分配和管理这些资源,以满足进程的需求。
  3. 任务隔离:每个进程都在独立的内存空间中执行,使得进程之间互不干扰。这种隔离性可以提高系统的稳定性和安全性,一个进程的错误不会影响其他进程。
  4. 进程间通信:进程可以通过进程间通信(IPC)机制来进行数据交换和协作。常见的IPC方式包括管道、消息队列、共享内存和套接字等。
  5. 多任务处理:操作系统可以根据调度算法来轮流分配CPU时间给不同的进程,实现多任务处理。这样,即使系统只有一个CPU,也可以让多个进程看起来同时运行。
  6. 进程状态管理:操作系统会跟踪每个进程的状态,并根据进程的活动(如创建、运行、挂起、终止等)进行管理和调度。

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

壳(Shell)是计算机操作系统中的一个命令行解释器,它是用户与操作系统内核之间的接口。其中,Bash(Bourne Again SHell)是一种常见的Unix和Linux系统下的壳。通过Bash壳,用户可以与操作系统进行交互,并执行各种操作和任务。

作用:

  1. 命令解释和执行:Bash接收用户输入的命令,并解释执行这些命令。它可以调用系统内建的命令、执行外部可执行文件、运行脚本等。
  2. 环境变量管理:Bash可以管理环境变量,包括设置、查看和修改环境变量的值。环境变量在操作系统中用于存储各种配置和参数信息。
  3. 脚本编程:Bash支持脚本编程,可以编写一系列的命令和控制结构,以实现自动化和批处理任务。Bash脚本可以使用条件语句、循环、函数等来实现复杂的逻辑。
  4. 输入输出重定向:Bash提供了输入输出重定向的功能,可以将命令的输入输出从标准输入输出设备(如键盘、屏幕)重定向到文件、管道等。

处理流程:

  1. 读取命令:Bash从标准输入中读取用户输入的命令。
  2. 解析命令:Bash对输入的命令进行解析,识别命令名称、参数、选项等。
  3. 执行命令:根据解析结果,Bash选择相应的处理方式。如果是内建命令(如cd、export等),Bash直接调用内核提供的相应功能执行。如果是外部命令,Bash会搜索用户设定的路径,找到对应的可执行文件并执行。
  4. 输出结果:命令执行完成后,Bash将结果输出到标准输出(屏幕)上供用户查看。用户还可以使用重定向将输出重定向到文件或管道中。
  5. 循环处理:Bash会持续读取用户输入的命令,循环执行上述处理流程,直到用户退出或发出终止信号。

6.3 Hello的fork进程创建过程

在终端中输入“./hello 2022113385 张馨方”后,父进程shell会对这条命令进行解析。shell判断hello不是内置指令,则认为是可执行文件,调用fork()创建子进程,子进程通过fork函数获得与父进程用户级虚拟地址空间相同的但是独立的副本,拥有不同的PID。接下来将hello加载到这个进程中执行。如果子进程结束时父进程仍然存在,那么将由父进程进行回收,反之则由init进行回收。进程结束时,子进程返回0,父进程返回子进程的PID。

6.4 Hello的execve过程

调用fork()函数创建了一个子进程后,execve函数在当前进程的上下文中加载并运行一个原型为int execve(const char *filename, const char *argv[], const char *envp[])的新程序(hello)。具体步骤:

  1. 子进程调用execve函数,并传入新程序的路径、命令行参数以及环境变量等参数。
  2. execve函数会首先根据给定的程序路径找到可执行文件,并将该文件加载到子进程的地址空间中。
  3. 子进程的地址空间会被新程序的代码和数据所替换,原有的代码和数据将被清除。
  4. 新程序开始执行,从其主函数开始执行。

需要注意的是,execve函数执行后,子进程的上下文会被新程序所替换,子进程的PID保持不变。也就是说,子进程的执行环境和资源被新程序所取代,但其进程标识符(PID)不会改变。如果新程序加载和执行成功,execve函数不会返回。如果加载和执行失败,execve函数会返回-1,并且可以通过errno全局变量获取错误码。通过调用fork函数创建子进程,再调用execve函数加载并运行新程序,可以实现进程的替换和执行不同的程序。

6.5 Hello的进程执行

Hello程序的进程执行过程是一个循环过程,操作系统根据调度算法分配时间片给进程,进程在用户态下执行循环中的代码,时间片耗尽后进行进程切换,切换到下一个进程执行。进程的具体执行过程如下:

  1. 进程上下文信息:每个进程都有一组上下文信息,包括进程的寄存器值、程序计数器、堆栈指针等。这些信息用于保存进程的执行状态,以便在进程切换后能够恢复到正确的执行点。
  2. 进程时间片:操作系统将CPU的执行时间划分为一段段的时间片,每个进程被分配一个时间片来执行。当时间片耗尽时,操作系统会进行进程切换,将CPU分配给另一个进程。
  3. 用户态与核心态转换:现代操作系统通常将进程的执行分为两个模式,即用户态和核心态。用户态中,进程只能执行受限的指令,不能直接访问系统资源。核心态中,进程拥有更高的权限,可以执行特权指令和访问系统资源。进程只有在核心态下才能进行一些特权操作,如修改内核数据结构。
  4. 进程调度过程:当Hello程序开始执行时,操作系统会为其创建一个进程并分配CPU时间片。Hello程序会进入用户态执行,按照循环中的代码,输出信息并暂停一段时间。
  5. 时间片耗尽:当Hello程序的时间片耗尽时,操作系统会进行进程调度,决定下一个要执行的进程。这个过程由调度算法决定,常见的调度算法包括轮转调度、优先级调度等。
  6. 进程切换:当操作系统决定切换到另一个进程时,它会保存当前进程的上下文信息,包括寄存器值、程序计数器等。然后,它会恢复下一个要执行的进程的上下文信息,并将CPU分配给该进程。
  7. 用户态与核心态转换:在进程切换时,如果需要进行特权操作,如访问系统资源,操作系统会将进程从用户态切换到核心态。这样,进程可以执行特权指令并访问相应的系统资源。
  8. 循环执行:新的进程开始执行,按照循环中的代码输出信息并暂停一段时间。这个过程会不断重复,直到所有进程都执行完毕或被终止。

6.6 hello的异常与信号处理

异常的种类:

hello执行过程中可能出现的情况:

正常运行:

乱按,但没有按到系统指令:

Ctrl+Z:

对应信号SIGSTP。

Ctrl+C:

对应信号SIGINT。

ps:

ps命令用于查看当前系统中运行的进程信息,它不直接发送信号给进程。

jobs:

jobs命令用于查看当前shell会话中的作业(即在后台运行的进程),它也不直接发送信号给进程。

pstree

pstree命令用于以树形结构显示进程之间的关系,它是一种进程查看工具,不发送信号给进程。

fg

fg命令用于将一个后台作业(即在后台运行的进程)切换到前台运行,它不直接发送信号给进程。

kill

kill命令用于向指定的进程发送信号,通过kill命令可以发送各种不同的信号给进程,常见的信号包括:

  1. SIGTERM(默认信号):终止进程,相当于发送终止请求给进程,进程可以捕获并进行清理操作。
  2. SIGKILL:强制终止进程,立即终止进程执行,不允许进程进行清理操作。
  3. SIGSTOP:暂停进程的执行,将进程挂起。
  4. SIGCONT:恢复进程的执行,将挂起的进程继续执行。

6.7本章小结

本章简述了进程的概念与作用,然后介绍了壳Shell-bash的作用与处理流程,并以可执行程序hello为例,介绍了其fork进程创建过程、execve过程、进程执行、异常与信号处理。


7hello的存储管理

7.1 hello的存储器地址空间

  1. 逻辑地址(Logical Address):逻辑地址是指程序中使用的地址,也称为虚拟地址。在Hello程序中,逻辑地址指的是程序中访问数据或指令的地址,比如字符串的地址或跳转指令的地址。
  2. 线性地址(Linear Address):线性地址是逻辑地址经过分段机制转换后得到的地址。在Hello程序中,逻辑地址先经过分段处理,将程序的地址空间划分为不同的段,然后将段内的偏移量与段的基地址相加,得到线性地址。
  3. 虚拟地址(Virtual Address):虚拟地址是指在操作系统虚拟内存管理下,程序所使用的地址。在Hello程序中,虚拟地址是指程序在逻辑地址空间中使用的地址,包括逻辑地址和线性地址。
  4. 物理地址(Physical Address):物理地址是最终由CPU发送给内存控制器的地址,用于访问实际的物理内存。在Hello程序中,物理地址是根据线性地址经过分页机制转换后得到的地址。分页机制将线性地址划分为固定大小的页,将页表中的页表项映射到物理内存的页框上,从而得到物理地址。

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

Intel处理器使用段式管理机制来将逻辑地址转换为线性地址。段式管理机制包括以下步骤:

  1. 段选择子:逻辑地址中包含一个段选择子,由段描述符索引和当前特权级别(CPL)组成。
  2. 段描述符:段描述符是存储在全局描述符表(GDT)或局部描述符表(LDT)中的数据结构。每个段描述符包含了段的基地址、大小、访问权限和类型等信息。
  3. 匹配段选择子和段描述符:根据逻辑地址中的段选择子的段描述符索引,从GDT或LDT中找到对应的段描述符。
  4. 线性地址计算:使用段描述符中的基地址和逻辑地址中的偏移量,计算得到线性地址。计算公式为:线性地址 = 段基地址 + 偏移量。
  5. 段限长检查:计算线性地址后,进行段限长检查,确保线性地址在段的边界内。

段式管理机制在保护模式下广泛应用,能够提供内存访问权限控制和隔离。然而,段式管理机制也存在一些缺点,比如段的大小固定和内存碎片化等问题。为了克服这些问题,现代操作系统通常会结合分页机制来进行地址转换。

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

Hello程序中,线性地址到物理地址的变换是通过页式管理机制完成的。这个过程涉及到将线性地址分配到页表中的页表项,并通过页表项中的物理页框地址和线性地址的偏移量计算得到物理地址。通过页式管理,可以将内存空间划分为固定大小的页,并将线性地址映射到相应的物理页框,实现对物理内存的访问。这种机制具有高效的内存管理和保护机制,可以有效地管理大容量的内存。

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

TLB(Translation Lookaside Buffer)是一个硬件高速缓存,用于加速虚拟地址(Virtual Address)到物理地址(Physical Address)的转换过程。在四级页表支持下,TLB与VA到PA的变换过程如下:

  1. 虚拟地址(Virtual Address):程序使用的地址称为虚拟地址,它由多级页表进行转换得到。
  2. TLB缓存:TLB是一个小而快速的高速缓存,用于存储最近使用的虚拟地址和物理地址之间的映射关系。TLB可以加速虚拟地址到物理地址的转换过程,避免频繁地访问页表。
  3. 多级页表:多级页表是一种数据结构,将虚拟地址划分为多个级别的索引,以便进行地址转换。每个级别的索引都对应一个页表。
  4. 页表项查找:根据虚拟地址的各级索引依次查找对应的页表项。通过多级页表的索引,可以逐级查找到最终的页表项。
  5. 物理地址计算:通过多级页表的索引,找到对应的页表项后,将物理页框地址与虚拟地址的偏移量相加,得到物理地址。
  6. TLB查询:在虚拟地址到物理地址的转换过程中,TLB会被检查以确定是否有虚拟地址到物理地址的映射关系。如果TLB中有对应的映射关系,那么可以直接从TLB中获取物理地址,避免了对页表的访问。

TLB与多级页表的结合可以提高地址转换的效率。TLB中存储了最近使用的虚拟地址和物理地址的映射关系,可以避免频繁地访问页表,从而加速地址转换过程。

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

在三级Cache支持下,物理内存的访问是通过多级Cache层次来加速的。这种层次化的Cache结构由L1、L2和L3三级组成,每一级的Cache容量逐级增大,但速度逐级降低。当CPU需要访问物理内存时,它首先会在L1 Cache中查找是否存在所需数据,如果未命中,则会进一步在L2 Cache中进行查找。如果在L2 Cache中也未命中,则会继续在L3 Cache中进行查找。如果依然未命中,则需要从主存中加载数据到L3 Cache,并通过L3 Cache传递给L2 Cache和L1 Cache,供CPU访问。通过这种三级Cache的层次化结构,可以大大提高CPU对物理内存的访问速度和效率。

7.6 hello进程fork时的内存映射

在hello进程执行fork()系统调用时,会创建一个子进程,子进程的内存映射是通过复制父进程的内存映射来实现的。以下是fork()时内存映射的简要过程:

  1. 父进程的内存映射:在执行fork()之前,父进程已经分配了一定的内存空间,包括代码段、数据段、堆、栈等。这些内存区域在父进程的虚拟地址空间中有相应的映射关系。
  2. 子进程的内存映射:当fork()被调用时,操作系统会创建一个新的进程,即子进程。子进程的内存映射会与父进程的内存映射完全相同。这是通过将父进程的页表复制到子进程中来实现的。这样,子进程将拥有与父进程相同的虚拟地址空间和相同的内存内容。
  3. 写时复制(Copy-on-Write):在fork()之后,父进程和子进程共享相同的物理内存页。当其中一个进程尝试修改共享的内存页时,操作系统会执行写时复制机制,将被修改的内存页复制到新的物理内存页中,使得父进程和子进程拥有各自独立的内存页。这样,父进程和子进程可以独立地修改它们自己的内存内容,而不会相互影响。

7.7 hello进程execve时的内存映射

在hello进程执行execve()系统调用时,会替换当前进程的内存映射,加载一个新的可执行文件。下面是execve()时内存映射的简要过程:

  1. 读取可执行文件:在执行execve()之前,操作系统会从文件系统中读取指定的可执行文件。这个可执行文件包含了程序的代码段、数据段、堆、栈等信息。
  2. 清除原有内存映射:在执行execve()时,操作系统会清除当前进程的原有内存映射。这意味着之前分配的内存空间会被释放。
  3. 创建新的内存映射:操作系统会根据可执行文件的内容和格式,创建新的内存映射。这包括将代码段、数据段、堆、栈等区域映射到进程的虚拟地址空间中。
  4. 加载可执行文件:操作系统会将可执行文件中的代码段加载到新的内存映射中的相应位置。数据段的内容也会被加载到新的内存映射中。
  5. 设置堆和栈:操作系统会根据可执行文件的要求,设置新的堆和栈的大小和位置。这样,程序可以使用新的堆和栈来分配和管理内存。

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

缺页故障(Page Fault)是指当程序访问某个虚拟地址所对应的页面不在物理内存中时发生的错误。当发生缺页故障时,操作系统会进行缺页中断处理,以下是缺页故障和缺页中断处理的简要过程:

  1. 缺页故障触发:当程序执行时,访问某个虚拟地址,但对应的页面并不在物理内存中时,就会触发缺页故障。这可能是因为页面被换出到磁盘上,或者是新分配的页面还未加载到物理内存中。
  2. 中断处理程序:当缺页故障发生时,处理器会产生一个异常,即缺页中断。操作系统会捕获该中断,并执行相应的中断处理程序。
  3. 中断处理程序执行:缺页中断处理程序首先会检查引发缺页故障的虚拟地址所对应的页表项。如果页表项中存在有效的物理地址,则将该地址映射到相应的虚拟地址,以满足程序的访问需求。
  4. 页面调入:如果页表项中不存在有效的物理地址,即页面不在物理内存中,操作系统需要将页面从磁盘上调入到物理内存。这涉及到磁盘I/O操作,将页面读取到物理内存中的某个空闲页面框中。
  5. 更新页表:页面调入后,操作系统会更新页表项,将虚拟地址和物理地址之间建立映射关系。这样,下次程序再次访问该虚拟地址时就可以直接找到对应的物理地址。
  6. 重新执行故障指令:在缺页中断处理程序完成页面调入和页表更新后,处理器会重新执行引发缺页故障的指令。这次访问就可以顺利地在物理内存中找到对应的页面,程序可以继续执行。

7.9动态存储分配管理

动态内存管理是指在程序运行时动态地分配和释放内存空间,以满足程序的内存需求。C语言中,常用的动态内存管理方法是使用malloc()函数分配内存空间,使用free()函数释放内存空间。以下是动态内存管理的基本方法和策略:

  1. 动态内存分配:使用malloc()函数可以在堆中分配一块指定大小的内存空间。malloc()函数的原型为void* malloc(size_t size),它返回一个指向分配的内存空间的指针。分配的内存大小可以根据程序的需要进行调整。
  2. 内存使用:一旦通过malloc()函数分配了内存空间,程序就可以使用该内存空间来存储数据。可以通过指针访问和操作这块内存空间。注意,动态分配的内存空间不会自动初始化,所以在使用之前需要确保正确地初始化内存。
  3. 内存释放:使用free()函数可以释放先前通过malloc()函数分配的内存空间。free()函数的原型为void free(void* ptr),它接受一个指向待释放内存空间的指针作为参数。一旦释放了内存空间,程序就不能再使用该内存空间,否则可能导致未定义的行为。
  4. 内存管理策略:动态内存管理还涉及一些策略,以优化内存使用和提高程序性能。常见的策略包括以下几个方面:
  1. 最佳适应(Best-fit):选择最接近所需大小的空闲内存块,以减少内存碎片。
  2. 首次适应(First-fit):从头开始搜索空闲内存块,选择第一个满足大小要求的内存块。
  3. 循环首次适应(Next-fit):从上一次分配位置开始搜索空闲内存块,选择第一个满足大小要求的内存块,适用于循环分配和释放的场景。
  4. 内存池(Memory pool):预先分配一块大的内存空间,然后根据需要从中分配小块内存,避免频繁的malloc和free操作,提高性能。

7.10本章小结

本章对hello的存储管理机制进行了梳理。具体介绍了hello的存储器地址空间,Intel逻辑地址到线性地址的变换-段式管理,Hello的线性地址到物理地址的变换-页式管理,TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,hello进程fork时的内存映射,hello进程execve时的内存映射,缺页故障与缺页中断处理,动态存储分配管理。


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:

在Linux中,设备被抽象为文件,即设备文件。每个设备都被视为一个文件,可以通过访问设备文件来进行设备的读取、写入和控制操作。设备文件通常位于/dev目录下,以文件名的形式表示设备。例如,硬盘设备可以表示为/dev/sda,串口设备可以表示为/dev/ttyS0。通过将设备模型化为文件,使得设备可以像操作文件一样进行读写操作,方便了设备的管理和访问。

设备管理:

Linux中的设备管理主要通过Unix I/O接口进行。Unix I/O接口是一组标准的系统调用函数,用于对设备进行读取、写入和控制操作。

常用的Unix I/O接口函数包括:

  1. open():打开设备文件,并返回一个文件描述符,用于后续的读写操作。
  2. read():从设备中读取数据。
  3. write():向设备中写入数据。
  4. ioctl():用于设备的控制操作,例如设置设备参数、获取设备状态等。
  5. close():关闭设备文件。

通过这些接口函数,程序可以直接对设备进行读写操作,并通过ioctl()函数进行设备的控制和设置。这使得设备的管理变得简单和灵活。此外,Linux还提供了设备驱动程序机制,允许开发者编写特定设备的驱动程序,以实现对设备的底层操作和管理。设备驱动程序通过与内核进行交互,实现设备的初始化、中断处理、数据传输等功能,从而提供给应用程序一个统一的接口。

8.2 简述Unix IO接口及其函数

Unix I/O接口是一组用于对设备进行读取、写入和控制操作的标准接口函数。这些函数通过系统调用来实现,提供了对文件和设备的统一访问方式。下面是常用的Unix I/O接口函数:

  1. open():打开文件或设备,并返回一个文件描述符。函数原型为int open(const char *pathname, int flags, mode_t mode)。其中,pathname表示文件或设备的路径,flags指定打开方式(如只读、只写、追加等),mode指定权限(仅在创建文件时生效)。
  2. close():关闭文件或设备。函数原型为int close(int fd),其中fd为文件描述符。
  3. read():从文件或设备中读取数据。函数原型为ssize_t read(int fd, void *buf, size_t count)。其中,fd为文件描述符,buf为接收数据的缓冲区,count为要读取的字节数。函数返回实际读取的字节数。
  4. write():向文件或设备中写入数据。函数原型为ssize_t write(int fd, const void *buf, size_t count)。其中,fd为文件描述符,buf为待写入的数据缓冲区,count为要写入的字节数。函数返回实际写入的字节数。
  5. lseek():设置文件或设备的读写位置。函数原型为off_t lseek(int fd, off_t offset, int whence)。其中,fd为文件描述符,offset为偏移量,whence指定相对起始位置(如文件开头、当前位置、文件末尾)。函数返回新的文件位置。
  6. ioctl():对文件或设备进行控制操作。函数原型为int ioctl(int fd, unsigned long request, ...)。其中,fd为文件描述符,request为控制请求码,后续参数根据不同的请求码而变化。该函数可以用于设备的特定操作,如设置设备参数、获取设备状态等。

8.3 printf的实现分析

printf函数是C语言中用于格式化输出的函数。下面是printf函数的实现分析:

  1. 格式化字符串处理:printf函数首先将格式化字符串中的字符和占位符进行解析,根据不同的占位符类型(如%d、%f、%s等)将对应的参数转换成字符串形式。
  2. 字符串生成:根据解析出来的格式化字符串和相应的参数,printf函数将生成最终的输出字符串。
  3. 输出字符串:生成的字符串需要通过IO设备进行输出。printf函数通常使用内部缓冲区来暂存字符串,当缓冲区满了或遇到换行符时,将缓冲区中的内容输出到IO设备上。
  4. IO设备输出:将生成的字符串通过IO设备(如终端、文件等)进行输出。这个过程一般会涉及到操作系统的IO接口,比如write系统函数或者陷阱-系统调用。
  5. 字符显示驱动:如果printf函数的输出是通过显示设备(如液晶显示器)进行显示,那么还需要经过字符显示驱动的处理。
  1. ASCII转字模库:将生成的字符串中的每个字符转换为对应的字模,即将字符的ASCII码映射为字模库中的对应字模。
  2. 字模到显示VRAM:将字模转换为显示VRAM中的对应点的RGB颜色信息,即将字模中的每个点映射为在VRAM中的位置和RGB颜色值。
  1. 显示芯片处理:显示芯片按照刷新频率逐行读取VRAM中的信息,并通过信号线向液晶显示器传输每一个点的RGB分量。显示芯片负责将VRAM中的点转换为液晶显示器上的实际像素。

8.4 getchar的实现分析

getchar函数是C语言中用于从输入设备中读取一个字符的函数。下面是getchar函数的实现分析:

  1. 键盘中断处理:当用户在键盘上按下一个键时,键盘控制器会产生一个中断信号,通知操作系统有键盘输入。操作系统会相应地执行键盘中断处理程序。
  2. 按键扫描码转换:键盘中断处理程序会读取键盘控制器中的按键扫描码,并将其转换为对应的ASCII码。这个过程可以使用键盘映射表来完成,根据扫描码查找对应的ASCII码。
  3. 键盘缓冲区:转换后的ASCII码会被保存到系统的键盘缓冲区中,以便之后的读取操作使用。键盘缓冲区是一个队列,按照按键的顺序保存按键的ASCII码。
  4. getchar调用read系统函数:当程序调用getchar函数时,会通过系统调用(如read函数)来读取输入设备中的字符。
  5. 读取按键ASCII码:read函数会从键盘缓冲区中读取一个字符的ASCII码。如果键盘缓冲区中没有字符,read函数会阻塞等待,直到有键盘输入为止。
  6. 等待回车键:getchar函数会循环调用read函数,直到读取到回车键(ASCII码为13)为止。这样可以确保getchar函数只返回用户按下回车键之前的字符。
  7. 返回字符:一旦读取到回车键,getchar函数会返回之前读取到的字符的ASCII码。

8.5本章小结

本章先介绍了Linux的IO设备管理方法,然后简述了Unix IO接口及其函数,对printf函数和getchar函数的实现进行了具体分析。

结论

Hello所经历的过程:

  1. 编写程序:程序员使用编辑器(如CS)编写名为"hello.c"的源代码文件。
  2. 预处理:编译器对"hello.c"进行预处理,包括对头文件的引入、宏替换等操作。
  3. 编译:预处理后,编译器将"hello.c"源代码转换为汇编代码。
  4. 汇编:汇编器将汇编代码转换为机器语言的指令。
  5. 链接:链接器将汇编生成的目标文件与需要的库文件进行链接,生成可执行文件。
  6. 操作系统管理进程:操作系统将可执行文件加载到内存中,并为其创建一个进程。
  7. 进程管理:操作系统为hello进程分配资源,如CPU时间片、内存空间等。
  8. 执行:操作系统将CPU的控制权交给hello进程,CPU开始执行hello进程的指令。
  9. 访问硬件资源:hello进程通过操作系统提供的接口访问硬件资源,如键盘、主板、显卡、屏幕等。
  10. 存储管理:操作系统与内存管理单元(MMU)一起,管理进程的虚拟地址到物理地址的映射,包括TLB、页表、缓存等。
  11. IO管理:操作系统负责管理输入输出设备,处理与设备的交互和数据传输。
  12. 完成任务:hello进程执行完程序中的任务(输出字符串"hello")后,将控制权交还给操作系统。
  13. 进程结束:操作系统回收hello进程所占用的资源,完成进程的结束。

通过本次大作业,我对计算机系统的结构有了更深刻的认识,在对hello生命周期的追溯中感受到了计算机的奇妙,对本专业领域的学习有了更加浓厚的兴趣。


附件

文件

描述

hello.i

hello.c预处理后的文件

hello.s

hello.i编译后的文件

hello.o

hello.s汇编后的文件

hello

hello.o链接后的文件

hello. elf

hello用readelf -a hello指令生成的文件

hello_o. elf

hello.o用readelf -a hello.o指令生成的文件


参考文献

[1] (CPU三级缓存技术解析_l3cache-CSDN博客.)

CPU三级缓存技术解析_l3cache-CSDN博客

[2]  (深入理解计算机系统-之-内存寻址(三)--分段管理机制(段描述符,段选择子,描述符表)_计算机的段描述-CSDN博客)

深入理解计算机系统-之-内存寻址(三)--分段管理机制(段描述符,段选择子,描述符表)_计算机的段描述-CSDN博客

[3] (linux edb使用手册,反汇编及linux下edb的下载-CSDN博客)

linux edb使用手册,反汇编及linux下edb的下载-CSDN博客

[4] (可执行目标文件的ELF格式)

 可执行目标文件的ELF格式 

[5](ELF可重定位目标文件 - mjy66 - 博客园)

 https://www.cnblogs.com/mjyrise/p/17747539.html 

[6] (c语言编译过程详解,预处理,编译,汇编,链接(干货满满)_预编译编译汇编链接-CSDN博客)

c语言编译过程详解,预处理,编译,汇编,链接(干货满满)_预编译编译汇编链接-CSDN博客

  • 17
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
哈尔滨工业大学(Harbin Institute of Technology,简称“哈工大”)是中国著名的重点大学,成立于1920年,是中国最早创办的六所工科高等学府之一。其中,哈尔滨工业大学的计算机科学与技术学院一直以来都是国内知名的学院。在其中,CSAPP是哈工大计算机科学与技术学院开设的一门经典课程,全称为《深入理解计算机系统》(Computer Systems: A Programmer's Perspective)。 这门课程涵盖了计算机系统的各个方面,从高级语言编程到机器级别的细节都有涉及,深入剖析了计算机系统的内部机制,讲解了各种计算机组件的原理,如内存、处理器、I/O设备、网络等等。此外,课程内容还包括缓存、异常、程序优化、并发编程、虚拟内存等重要主题,并且还会涉及安全问题,例如注入攻击、缓冲区溢出等等。 相较于其他计算机相关的课程而言,CSAPP的特殊之处在于,它以程序员的视角,深入而生动地解释了计算机系统的工作方式和内部机制。课程强调了实践性,通过大量的例子及编程作业,学生可以实际操作并理解到具体的计算机系统的运行方式。 此外,CSAPP的教学团队非常强大,由哈工大的多位顶尖教授组成,能够保证教学质量和深度。学生通过学习这门课程,不仅可以深入了解计算机系统的各个方面,还可以提高编程能力和工程实践水平,有助于更好地应对工作中遇到的各种问题。 总之,CSAPP是哈尔滨工业大学计算机科学与技术学院开设的一门经典课程,其全面而深入的课程内容、强调实践性、优秀的教学团队等特色让其在国内享有较高声誉,对学生深入理解计算机系统、提高编程实践能力等方面,都有非常积极的作用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值