HIT-CSAPP大作业 hello.c的一生

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业        信息安全          

学     号         2022112266        

班     级         2203202           

学       生          魏圣卓           

指 导 教 师           史先俊            

计算机科学与技术学院

2024年5月

摘  要

        本文详细介绍了Hello's P2P项目的开发过程,从程序到进程的转变。整个过程从预处理、编译、汇编到链接,每一步都进行了详细的解析和展示。首先,预处理阶段处理hello.c文件,生成hello.i文件,包括删除注释和多余空白字符,替换头文件和宏定义等。接着,编译阶段将hello.i编译为hello.s汇编文件,生成汇编指令和数据段的映射。然后,通过汇编器将hello.s转化为可重定位目标文件hello.o,并对其进行反汇编和ELF格式解析。最后,通过链接器将hello.o与其他目标文件连接,生成最终的可执行文件hello。在整个过程中,报告还介绍了虚拟地址空间、动态链接、进程管理、存储管理和IO管理的实现。通过详尽的步骤和分析,本文展示了从源代码到可执行文件的完整转变过程。

关键词:C语言;编译过程;计算机系统

目  录

第1章 概述................................................................................................................ - 4 -

1.1 Hello简介......................................................................................................... - 4 -

1.2 环境与工具........................................................................................................ - 4 -

1.3 中间结果............................................................................................................ - 4 -

1.4 本章小结............................................................................................................ - 4 -

第2章 预处理............................................................................................................ - 5 -

2.1 预处理的概念与作用........................................................................................ - 5 -

2.2在Ubuntu下预处理的命令............................................................................. - 5 -

2.3 Hello的预处理结果解析................................................................................. - 5 -

2.4 本章小结............................................................................................................ - 5 -

第3章 编译................................................................................................................ - 6 -

3.1 编译的概念与作用............................................................................................ - 6 -

3.2 在Ubuntu下编译的命令................................................................................ - 6 -

3.3 Hello的编译结果解析..................................................................................... - 6 -

3.4 本章小结............................................................................................................ - 6 -

第4章 汇编................................................................................................................ - 7 -

4.1 汇编的概念与作用............................................................................................ - 7 -

4.2 在Ubuntu下汇编的命令................................................................................ - 7 -

4.3 可重定位目标elf格式.................................................................................... - 7 -

4.4 Hello.o的结果解析.......................................................................................... - 7 -

4.5 本章小结............................................................................................................ - 7 -

第5章 链接................................................................................................................ - 8 -

5.1 链接的概念与作用............................................................................................ - 8 -

5.2 在Ubuntu下链接的命令................................................................................ - 8 -

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

5.4 hello的虚拟地址空间..................................................................................... - 8 -

5.5 链接的重定位过程分析.................................................................................... - 8 -

5.6 hello的执行流程............................................................................................. - 8 -

5.7 Hello的动态链接分析..................................................................................... - 8 -

5.8 本章小结............................................................................................................ - 9 -

第6章 hello进程管理....................................................................................... - 10 -

6.1 进程的概念与作用.......................................................................................... - 10 -

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

6.3 Hello的fork进程创建过程......................................................................... - 10 -

6.4 Hello的execve过程..................................................................................... - 10 -

6.5 Hello的进程执行........................................................................................... - 10 -

6.6 hello的异常与信号处理............................................................................... - 10 -

6.7本章小结.......................................................................................................... - 10 -

第7章 hello的存储管理................................................................................... - 11 -

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 -

7.9动态存储分配管理........................................................................................... - 11 -

7.10本章小结........................................................................................................ - 12 -

第8章 hello的IO管理.................................................................................... - 13 -

8.1 Linux的IO设备管理方法............................................................................. - 13 -

8.2 简述Unix IO接口及其函数.......................................................................... - 13 -

8.3 printf的实现分析........................................................................................... - 13 -

8.4 getchar的实现分析....................................................................................... - 13 -

8.5本章小结.......................................................................................................... - 13 -

结论............................................................................................................................ - 14 -

附件............................................................................................................................ - 15 -

参考文献.................................................................................................................... - 16 -

第1章 概述

1.1 Hello简介

P2P:即从程序到进程。指的是从hello.c(程序)变成运行时的进程。要使hello.c这个C语言程序运行起来,需要先将其转化为可执行文件。首先将hello.c文件进行预处理,生成hello.i文件。然后通过编译器将hello.i编译为汇编的hello.s文件。再通过as将hello.s转化为可重定位目标文件hello.o。最后通过连接器将hello.o和其他目标为念进行连接,最终创建出可执行文件。完成这些阶段后,就会得到一个可执行文件,然后可以在shell中执行它。执行时,shell会为它分配进程空间。

020:即从零开始到零结束。指的是最初内存中没有hello文件的相关内容,shell使用execve函数启动hello程序,将虚拟内存映射到物理内存,并从程序入口开始加载和运行,执行main函数中的目标代码。程序结束后,shell的父进程回收hello进程,内核删除hello文件相关的数据结构,清空该程序所占的空间,抹去所有痕迹。这就实现了020:From Zero to Zero。

1.2 环境与工具

OS: Ubuntu 22.04.3 LTS x86_64

Kernel: 5.15.0-88-generic

Shell: zsh 5.8.1 

CPU: Intel Xeon Platinum (2) @ 2.499GHz

开发与调试工具:nano objump edb gcc 等工具

1.3 中间结果

hello.i         预处理后得到的文本文件

hello.s         编译后得到的汇编语言文件

hello.o        汇编后得到的可重定位目标文件

hello.asm      反汇编hello.o得到的反汇编文件

helloexe.asm     反汇编hello可执行文件得到的反汇编文件

hello.elf      hello.o的elf文件

helloexe.elf     hello的elf文件

1.4 本章小结

本章主要介绍了该次实验的软硬件环境和中间文件的内容,为后续开发打下基础。

解释了P2P和020的概念,后面对hello程序的分析正是由这两个概念而来的。

第2章 预处理

2.1 预处理的概念与作用

在程序运行前,预处理器需要对源文件进行初步处理,主要包括代码文本的替换工作。例如,处理以 # 开头的指令,同时删除程序中的注释和多余的空白字符。预处理器不会直接解析程序源代码的内容,而是对源代码进行相应的分割、处理和替换。

头文件包含:将 #include 指令替换为头文件中的实际代码。

识别宏定义:预处理器扫描源代码,寻找用 #define 指令定义的宏。预处理器在代码中遇到宏时,将其替换为宏定义中的内容。

条件编译:根据条件判断是否编译某段代码。

注释删除:删除代码中的注释和多余的空行。

预处理是编译过程中必不可少的步骤,通过宏定义、文件包含、条件编译等手段,使代码更加灵活、模块化和易于管理。理解预处理的概念和作用对于编写高效、可维护的C/C++代码至关重要。

2.2在Ubuntu下预处理的命令

gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i

 

图1 预处理命令

2.3 Hello的预处理结果解析

图2 hello.c的预处理结果hello.i

图3 外链函数部分

图4 预处理文件中的主函数部分

主函数没有变化,将前面的头文件添加了进来,按次序添加进hello.i中

以 `stdio.h` 为例,在预处理过程中,当预处理器扫描到 `#include <stdio.h>` 后,会在系统的头文件路径下查找 `stdio.h` 文件,并将其内容直接复制到代码中。

如果头文件中也包含了其他的头文件,则预处理器也会将其添加进预处理后的文件中。

2.4 本章小结

本章介绍了程序预处理的过程,主要解释了编译器如何将程序进行预处理,替换其中的头文件。在这其中不包含任何计算和处理,只是简单拿的复制和替换操作。 预处理的意义在于允许我们在编写程序时仅关注真正实现功能的那部分代码,而不需要关注其他头文件等部分。

第3章 编译

3.1 编译的概念与作用

编译是将高级编程语言编写的源代码转换为目标机器语言或中间代码的过程。这个过程由编译器完成,编译器是专门设计用于将源代码翻译成计算机可以执行的机器代码的软件工具。

编译的主要步骤:

词法分析:将源代码转换为记号序列,这些记号是程序的基本语法成分,如关键字、变量名、操作符等。

语法分析:根据语言的语法规则,将记号序列转换为语法树或抽象语法树,表示程序的语法结构。

语义分析:检查程序的语义是否正确,如类型检查、变量作用域检查等,确保程序的逻辑符合语言规范。

中间代码生成:将语法树或抽象语法树转换为中间代码,这种代码介于高级语言和机器语言之间,便于进一步优化和翻译。

代码优化:优化中间代码,提高程序执行效率或减少资源消耗,优化可以在多个层次进行,如局部优化、全局优化、循环优化等。

目标代码生成:将优化后的中间代码转换为目标机器语言代码(机器码或字节码)。

汇编和链接:将目标机器语言代码转换为可执行文件,汇编器将汇编语言代码转换为机器码,链接器将不同模块和库结合在一起,生成最终的可执行文件。

3.2 在Ubuntu下编译的命令

图5 对hello.i进行编译

3.3 Hello的编译结果解析

汇编头部分

图6 汇编头部分

- `.file`:声明源文件。

- `.text`:表示代码段。

- `.section .rodata`:表示只读数据段。

- `.align`:声明指令或数据的对齐方式。

- `.string`:声明一个字符串。

- `.globl`:声明全局变量。

- `.type`:声明一个符号的类型。

数据部分:

图7 数据部分

将字符串LC0和LC1存放进只读数据端

全局函数:

图8 全局函数部分

此处标记了hello程序的全局函数,.global标记了main为全局变量,.type标记main为一个函数。标志该程序的入口为main。

图9 主函数部分

将字符串的起始地址放进rax中,然后放入rdi中

赋值部分:

图10 汇编的赋值语句

这里存储的argc参数,用于记录传入参数的数量,存放在edi中,再保存到-20(%rbp)

图11 汇编赋值语句

将输入参数argv保存到rsi中,再存到-32(%rbp)。然后程序进行比较%rbp-20是否为5,否则退出,这里对应源程序中的if。

赋值操作:

图12 跳转语句

此处-4(%rbp)存放程序使用的局部变量i。

Movl指令将0存入rbp-4,对应原始函数中for循环的i=0

算术操作:

图13 算术操作

同样的,该步addl对应原for循环中的i++

关系操作:

图14 函数对应关系

此处对应原函数的if(argc==5)判断,如果参数argc为5则步入L2,否则运行下面的程序,退出函数。

图15 退出函数部分

此处对应原函数的for循环,当i<10时,程序步入L4,执行操作函数,否则执行下面的代码退出循环。

控制转移操作:

图16 控制转移函数1

图17 控制转移函数2

主要是上面提到的控制跳转函数这部分

函数操作部分:

Printf

图18 函数操作部分

其中使用movq  -32(%rbp), %rax  addq   $24, %rax addq   $16, %rax addq   $8, %rax等操作依次调用argv1、2、3这三个参数

Puts:

图19 puts函数对应汇编

程序判断跳出时,直接使用了puts。将对应的字符串写入rdi中,然后调用puts直接输出。

Atoi和sleep

图20 atoi和sleep函数对应汇编

Atoi将字符串数字转换成sleep所需的整形数字

3.4 本章小结

本章简单介绍了编译器将.i文件编译为.s文件的过程,主要涉及将c语言diamagnetic转化为汇编代码。主要涉及函数调用,数据处理,复制算数等操作。

但是在其中我们也能注意到编译器并不是完全按照原有的c语言对代码进行转换的。比如在argc!=5的情况下,编译器调用的函数为puts而不是printf,这是因为在只输出一个简单字符串的情况下,puts的开销会更小,算是节约程序运行开销的一部分操作。

第4章 汇编

4.1 汇编的概念与作用

汇编是一种低级语言,用于将人类可读的指令转换为计算机可执行的机器码。在汇编语言中,每条指令都对应着计算机处理器的一条基本操作,如加载数据、进行运算、存储结果等。

汇编的主要作用:接近硬件:汇编语言直接映射到计算机的指令集架构,能够更直接地控制计算机的硬件。

可移植性:汇编语言相对于机器语言来说,更容易理解和修改,同时也具有一定的可移植性,因为在不同的硬件平台上,可以通过修改汇编代码来适配不同的指令集。

优化性能:通过手动编写汇编代码,程序员可以更加精细地控制程序的执行流程和内存访问,以达到优化程序性能的目的,特别是在对性能要求极高的应用领域,如嵌入式系统、游戏开发等。

理解底层原理:学习汇编语言有助于理解计算机系统的底层原理,包括指令执行过程、内存管理、寄存器使用等,从而更好地理解高级语言的工作原理和优化方法。

调试与分析:在调试程序时,通过查看汇编代码可以更直观地了解程序的执行过程,帮助识别和解决问题。

汇编代码中还生成了一些能够指引链接过程进行的数据结构,这些数据结构类似于装配指南或说明书,指示链接器如何将各个部分正确地组装在一起。

通过汇编和链接的详细描述,我们可以看到程序从高级语言到最终可执行文件的完整转变过程。这不仅包括简单的翻译,还涉及符号解析、地址重定位和库文件的处理。

4.2 在Ubuntu下汇编的命令

gcc -c -m64 -no-pie -fno-PIC  hello.c -o hello.o

图21 将hello.s汇编为hello.o

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

图22 生成hello.o的elf文件

图23 hello.o的elf文件头

Elf头中主要包括程序起点,整体描述,文件类型,机器类型等信息

图24 字节头中信息

字头部分主要记录各节名称、类型、地址、大小等信息

图25 重定位部分

重定位部分主要表示连接器将本文件和其他文件组合时的参数,当调用外部函数或引用全局变量时需要修改此部分。在最后编译出的可执行文件部分不包括重定位信息。

图26 symbol表部分

Symboltable符号表,主要包括程序的定义类型和全局变量等信息。

4.4 Hello.o的结果解析

objdump -d -r hello.o 

图27 反汇编hello.o

当汇编器生成 `hello.o` 文件后,它并不知道数据和代码最终会被放置在内存中的确切位置。它也不知道这个模块引用的任何外部定义的函数或全局变量的位置。因此,每当汇编器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。这个重定位条目告诉链接器在将目标文件合并成可执行文件时,如何修改这些引用。

对照分析部分:

图28 hello.s和hello.asm对比

反编译后的指令中添加了对应的机器码,使用hex表示。

 

图29&图30 反汇编对比

转化了进制,将数字同样转化成hex形式。

图31&图32 反汇编函数对比

在.s文件中函数使用诸如.L2 .L4等形式区分成块,并互相调用

在反编译文件中函数跳转使用相对偏移量来表示,如<main+0x8d>,不再以函数名称互相调用。

4.5 本章小结

本章主要简单叙述的汇编语言变为可重定位目标文件的过程,我们将.s文件汇编成.o文件,再将其转化为.elf文件进行分析。通过对比.s文件和反汇编生成的.asm文件,我们可以得知汇编语言转化为机器码的过程。以及可重定位文件为了链接而做的准备。

ELF文件结构包括三个主要部分:ELF 头、程序头表和节头表。

ELF 头:位于文件开头,包含了文件的基本信息,如文件类型、机器架构、入口地址等。包括了各种字段,如文件标识、文件类型、机器类型、入口地址、程序头表偏移、节头表偏移等。

程序头表:描述了如何在内存中加载文件的各个段(segment),如代码段、数据段等。对于可执行文件和共享库是必需的,对于目标文件是可选的。每个表项描述了一个段的相关信息,如段的类型、偏移、虚拟地址、文件大小、内存大小等。

节头表:描述了文件中的各个节,如代码节、数据节、符号表节等。对于链接器和调试器是必需的。每个表项描述了一个节的相关信息,如节的名称、类型、标志、偏移、大小、链接等。

ELF 文件结构允许灵活地组织文件内容,并提供了丰富的信息,以便于加载、链接和调试。ELF 格式被广泛应用于 Linux 和许多其他操作系统中,用于表示可执行文件、目标文件、共享库等。

链接器是编译过程中的最后一个阶段,它负责将编译生成的目标文件合并成最终的可执行文件、共享库或静态库。

符号解析:链接器首先会对所有目标文件中的符号进行解析。符号可以是函数、全局变量或外部变量的引用或定义。如果一个符号在当前目标文件中被定义,链接器将在符号表中为它分配一个地址;如果一个符号在当前目标文件中被引用但未定义,链接器将在后续的步骤中解析这些未解析的引用。

地址重定位:链接器根据符号解析的结果,将目标文件中的每个符号引用重定位到其对应的定义处。这涉及修改目标文件中的代码和数据,以确保它们正确地访问其他目标文件中的符号。

符号合并:如果多个目标文件中都定义了相同的符号,链接器将合并这些符号的定义,以避免重复定义的冲突。如果存在重复定义的情况,链接器可能会发出错误或警告,提示开发者解决冲突。

节合并:链接器将各个目标文件中的节合并成一个或多个输出文件中的节。这涉及将相同类型的节(如代码节、数据节)合并,并对合并后的节进行排序和调整,以确保最终的可执行文件具有正确的布局和结构。

重定位表生成:链接器生成重定位表,其中包含了所有需要进行重定位的位置和对应的重定位类型。这些重定位信息在加载可执行文件时用于动态地修改代码和数据,以便正确地引用其他符号。

生成输出文件:最后,链接器将所有合并后的代码和数据写入输出文件中,这个输出文件可能是可执行文件、共享库或静态库,具体取决于链接器的输入和参数。

链接器的主要目标是将多个目标文件合并成一个完整的可执行文件或库文件,同时解决符号引用和地址重定位等问题,以确保最终的程序可以正确地执行和链接。

第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 /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o hello.o

图33 链接hello.o

生成hello可执行文件

图34 查看hello格式为可执行文件

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

图35 生成hello的elf文件

图36 查看hello的elf文件头

Elf头内容基本相同,主要新增了程序头大小,入口地址等信息,同时类型等信息有所改变

图37 查看helloelf文件的节头

节头将各个文件的相同段进行合并,生成一个大段,并且根据该段大小shezhi其他段的偏移量,最终生成一个完整的节头。

图38 查看elf程序头部分

程序头描述了系统执行程序的准备过程

图39 查看动态头部分

动态头包含了动态链接器在运行时需要的信息。每个条目由一个标签和一个值组成,描述了各种动态链接所需的资源和操作。

图40 重定位段部分

重定位段包含了需要在加载时或运行时修正的地址信息。

图41 符号表部分

符号表包含了程序中所有符号的信息,如变量和函数。.dynsym 是动态符号表,包含了在运行时动态链接器需要处理的符号信息。

5.4 hello的虚拟地址空间

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

图42 查看hello的空间

图43 edb查看程序代码地址

查看程序代码对应在0x401000位置

图44&图45 查看text位置

Text对应的0x4010f0位置

图46 查看各段地址

程序段对应在0x400000-0x405000

链接的lib对应在0x7f4009653000-0x7f400968f000

Stack对应在0x7ffe28048000-0x7ffe28069000

5.5 链接的重定位过程分析

图47 生成hello的反汇编文件

链接生成可执行文件后,反汇编文件中多了.plt puts@plt printf@plt等函数代码,因为最终生成可执行文件时需要调用这部分代码,所以链接器将其动态的加入elf文件中。

图48&图49 查看反汇编函数对比

同时注意到函数跳转的形式也发生了变化,从相对偏移量的表示法变成了重定位后的函数表示法。

重定位可以分为两步:

1. 重定位节和符号定义:链接器将所有相同类型的节合并成一个聚合节,并为其分配运行时内存地址。这一步还包括更新符号表中的符号定义,以反映这些符号在新聚合节中的位置。

2. 重定位引用:链接器调整代码和数据中所有对符号的引用,使它们指向正确的内存地址。链接器确定每个符号在最终可执行文件中的地址,包括将所有外部符号解析为它们在链接的目标文件中的地址,使用符号表完成解析。链接器为每一个需要重定位的引用计算目标地址,可能需要从符号表中查找符号地址,并将这些地址加上相应的偏移量。链接器根据计算出的地址更新所有需要重定位的条目,这些条目通常在目标文件的重定位表中列出,链接器根据这些表修改代码和数据段中的地址。链接器在代码和数据段中应用重定位条目,将所有相对地址和绝对地址修正为运行时的实际地址,确保所有函数调用和全局变量访问等操作都能正确指向最终地址。

5.6 hello的执行流程

程序首先进入_start函数

图50程序进入_start函数

然后步入_lib_star_main部分

图51 程序步入_lib_star_main部分

此时call main,步入main函数,正常执行程序

图52 程序进入main函数

如果参数输入正确,程序会进行printf  -> sleep -> getchar -> exit

如果参数输入不正确,程序main -> puts -> exit

子程序名和地址如下:

图53 子程序名及地址

5.7 Hello的动态链接分析

   (以下格式自行编排,编辑时删除

分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。

Hello通过got和plt表实现动态链接

动态连接的目的是将程序变成相对独立的模块,类似于一个个函数,在程序执行时才将其链接到一起。

当某些函数被频繁调用时,动态链接技术可以使其不必多次存在于代码之中,而是使用链接保存函数的重定位位置,当执行时在调出地址执行。其中使用的两个辅助数据为plt和got,分别存放在代码段和数据段。

图54 plt和got地址

此处查看到got表地为0x403ff0

plt表地址为0x401020

GOT 是一个存储全局变量和函数地址的表。每个动态链接的程序或库都有一个 GOT。它的主要作用是存储被动态链接库引用的函数和变量的地址。

初始化:在程序启动时,GOT 初始化为指向运行时链接器(如 ld.so)的地址。这个链接器负责在第一次调用时解析符号。

懒加载(Lazy Binding):当程序首次调用一个动态链接的函数时,GOT 中的相应条目会指向一个特殊的 PLT 段,该段会调用运行时链接器。

符号解析:运行时链接器解析符号并将正确的函数地址填入 GOT 表中。

后续调用:后续对该函数的调用直接使用 GOT 表中的地址,避免了再次进行符号解析,从而提高运行效率。

PLT 是一系列跳转指令表,用于处理对动态链接库函数的调用。PLT 条目与 GOT 紧密配合,特别是在符号解析过程中。

初始调用:当一个函数首次被调用时,调用会跳转到对应的 PLT 条目。

跳转到链接器:PLT 条目包含的指令会跳转到 GOT 表中的一个特殊入口,这个入口最初指向运行时链接器。

调用运行时链接器:运行时链接器负责解析符号,找到实际的函数地址,并将这个地址写入 GOT 表的相应位置。

更新 GOT:PLT 中的指令会跳转到更新后的 GOT 表地址,这样后续调用直接跳转到正确的函数地址。

PLT 和 GOT 的交互

初次调用:例如,程序调用一个动态链接的函数 printf。程序首先跳转到 printf 在 PLT 中的入口。

PLT 跳转:PLT 中的指令会跳转到 GOT 表中的一个特殊入口,该入口指向运行时链接器。

符号解析:运行时链接器解析 printf 的实际地址,并将这个地址写入 GOT 表中的相应位置。

更新跳转:PLT 中的指令会再次跳转,这次跳转会根据更新后的 GOT 表直接跳转到 printf 的实际地址。

后续调用:以后的 printf 调用将直接使用 GOT 表中的地址,避免再次调用运行时链接器。

这种机制的优点是第一次调用时虽然有额外的开销(符号解析),但后续调用的开销很小,从而提高了程序的运行效率。

5.8 本章小结

链接这一过程是将已经生成的目标程序的可重定位文件和其所需要调用的一众函数可重定位文件进行合并,最终生成一个可执行文件。在链接整个项目时,通常会将多个不同的目标文件和目标函数进行串联组合,按照其互相引用关系最终生成一个可执行文件。在gcc中,链接由ld自动执行,将构成一个最终程序的代码分离为一个个相对独立的模块,并且可以独立的修改和编译这些模块。最终实现程序的模块化和易修改化。

第6章 hello进程管理

6.1 进程的概念与作用

进程是操作系统中资源分配的基本单位,是一个正在执行的程序实例。它不仅包括程序的可执行代码,还包括程序的运行时所需的所有资源,如内存、文件描述符、CPU时间等。每个进程都有一个唯一的进程标识符,用于区分不同的进程。

进程的生命周期:

创建:进程由操作系统创建,通常由现有的进程通过系统调用(如fork或spawn)生成。

执行:进程执行其指令,操作系统调度器管理进程的执行顺序和时间。

等待:进程可能会进入等待状态,等待某些事件(如I/O操作完成)或资源(如内存)可用。

终止:进程完成任务或被强制终止,操作系统回收其资源。

进程的主要作用:

资源管理和分配:

内存管理:进程有自己的地址空间,包括代码段、数据段、堆栈段等,操作系统为每个进程分配和管理所需的内存资源。

CPU时间:操作系统调度器为每个进程分配CPU时间,以便多个进程可以并发执行。

文件和I/O管理:进程可以打开、关闭、读取和写入文件,操作系统管理这些文件描述符和I/O操作。

程序执行环境:

独立运行:进程提供了一个独立的运行环境,保证一个进程的执行不会直接影响其他进程。这种隔离性提高了系统的稳定性和安全性。

多任务处理:通过进程,操作系统可以在单个CPU上实现多任务处理,即使在多核CPU上,也能更有效地利用硬件资源。

安全和权限控制:

用户权限:每个进程都运行在特定的用户权限下,操作系统通过用户身份和权限控制进程对系统资源的访问。

隔离性:进程间的隔离性使得一个进程的错误(如崩溃或内存泄漏)不会直接影响其他进程,增强了系统的安全性。

进程间通信(IPC):

通信机制:进程间可以通过各种IPC机制(如管道、消息队列、共享内存、信号等)进行数据交换和同步,协同完成复杂的任务。

数据共享:通过共享内存等方式,多个进程可以高效地共享数据。

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

Shell是用户与操作系统之间的交互界面,它接受用户输入的命令并将其传递给操作系统进行执行。Shell 既可以作为命令解释器来执行用户输入的命令,也可以作为编程环境来编写和执行脚本。Shell 作为用户与操作系统之间的桥梁,解释和执行用户输入的命令,管理用户环境,支持脚本编写和执行,通过上述处理流程,实现命令解析、查找和执行,提供了一个功能强大且灵活的交互和编程环境。

当用户向shell输入指令后,shell首先判断该命令是否为内置命令,如果是则执行,否则使用fork创建新的进程来执行程序,如果该程序为前台程序则执行时等待,结束时返回。如果为后台程序则创建后直接返回。执行过程中shell也可以对用户输入进行处理,例如使用ctrl+c来终止执行。

6.3 Hello的fork进程创建过程

用户输入 ./hello 2022112266 魏圣卓 18745678901 1

Shell对命令进行判断,识别为外部指令,则调用fork产生新的子进程,执行此命令。

6.4 Hello的execve过程

1.父进程调用 execve

系统调用原型:int execve(const char *pathname, char *const argv[], char *const envp[]);

pathname:要执行的程序的路径

argv:指向参数列表的指针数组,第一个参数通常是程序名。

envp:指向环境变量列表的指针数组。

2.系统调用接口:

通过系统调用接口,进程从用户态切换到内核态。

内核接管控制权,并验证传递的参数,确保文件存在且可执行。

3.加载可执行文件:

内核读取 hello 文件头,检查其格式(如 ELF)。

根据 ELF 头信息,内核加载程序的代码段、数据段等到内存中。

4.设置新进程环境:

内核为新程序分配内存空间,包括堆、栈等区域。

将传递的参数 argv 和环境变量 envp 复制到新进程的地址空间。

初始化进程的用户栈,准备好参数和环境变量。

5.替换进程镜像:

当前进程的内存空间被新程序替换。

旧的代码段、数据段、堆栈等被卸载。

6.设置入口点:

根据可执行文件头信息,设置程序的入口点(通常是 _start 函数)。

初始化 CPU 寄存器,将程序计数器(PC)设置为入口点地址。

7.执行新程序:

内核态返回用户态,新程序开始执行,从入口点开始运行。

6.5 Hello的进程执行

在运行 `hello` 程序时,操作系统为应用程序提供了以下抽象:

1. 独立的逻辑控制流:使得进程看起来像是独占地使用处理器。

2. 私有的地址空间:使程序仿佛独占地使用内存。

操作系统提供的具体抽象包括:

1. 逻辑控制流:用户在调试器中看到的程序计数器(PC)值的序列,称为逻辑控制流或逻辑流。当多个逻辑流在时间上重叠执行时,它们被称为并发流。

2. 上下文切换:内核通过上下文切换机制实现多任务处理。内核为每个进程维护一个上下文,即重启被抢占进程所需的状态信息。

3. 时间片:进程在每个时间片内执行其控制流的一部分,因此多任务处理也被称为时间分片。

4.用户模式和内核模式:处理器通过模式位来区分内核模式和用户模式。内核模式下,进程可以执行所有指令并访问任何内存;用户模式下,进程不能执行特权指令或直接访问内核内存。

5.上下文信息:包括通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构的值。执行 `hello` 程序时,调用 `execve` 分配新的虚拟地址空间,程序在用户模式下运行并调用 `printf` 输出信息。随后调用 `sleep` 函数,进程进入内核模式处理信号,然后返回用户模式。CPU通过上下文切换,在多个时间片中与其他进程交替使用,实现进程调度。

图55 上下文切换

6.6 hello的异常与信号处理

       异常分类:

异常主要包括中断、故障、终止等。运行hello中容易遇到的终止(用户终止)

图56 中断异常处理

图57 陷阱异常处理

图58 故障异常处理

图59 中止异常处理

信号处理

信号是一种进程间通信机制,用于通知进程某些事件的发生。信号可以由操作系统、用户或进程自身生成。常见的信号包括:

SIGINT:中断信号(Ctrl+C)

SIGKILL:终止信号,不能被捕获或忽略

SIGTERM:终止信号,可以被捕获或忽略

SIGSEGV:无效内存访问(段错误)

处理方式:

中断处理:指令执行过程中收到中断信号,当前指令完成后,控制传递给处理程序,中断处理程序运行,直到处理程序返回到下一条指令

故障处理:指令出现故障后,控制传递给处理程序,故障处理程序运行,会决定继续处理程序或是直接终止,最后返回

终止处理:当前指令发生致命错误时,传递控制给处理程序,终止处理程序运行,返回到abort事件例程

运行演示:

图60 正常运行

图61 运行时ctrl+c中止

Shell接收到sigint信号,中止当前进程

图62 运行时ctrl+z暂停

Shell收到sigstp信号,暂停当前进程并挂起,回到shell程序

图63 使用ps和jobs查看已挂起的进程

图64 Pstree查看到进程树

图65 使用kill结束进程

图66 执行程序并挂起,使用fg恢复进程

图67 运行时输入,会有对应显示

6.7本章小结

本章主要解释了进程的工作原理和shell的执行过程,通过对hello程序的中止,挂起,恢复等操作了解到了一个进程的创建、启动、执行的过程。同时解释了程序运行过程中可能遇到的异常情况和各种输入的情况。程序的正常运行是由各种进程和信号来保证的。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:由程序生成的地址,通常与虚拟地址同义。例如,在 hello 程序中,printf 函数调用中的地址使用的是逻辑地址。

线性地址:通过段选择器和段偏移量生成。在现代操作系统中,段寄存器基址为0,所以逻辑地址等于线性地址,形成一个连续的地址空间。

虚拟地址:在现代操作系统中,通常等同于逻辑地址。操作系统通过页表将虚拟地址映射到物理内存,每个进程都有独立的虚拟地址空间。例如,hello 程序的 main 函数和 printf 函数的地址都是虚拟地址。

物理地址:实际的内存地址,由 MMU 将虚拟地址转换而来。用户程序无法直接访问,必须通过虚拟地址间接访问。

地址变换原理

在程序进行编译和链接时,会生成逻辑地址,在ELF文件中表现为段内偏移量。在程序加载时会将逻辑地址映射到该进程的虚拟地址空间,生成线性地址。在运行时处理器将线性地址转换为物理地址,最终访问物理内存中的实际数据

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

Intel的x86架构中,逻辑地址到线性地址的转换通过段式管理实现。段式管理使用段寄存器和段描述符来实现这一转换。以下是详细的变换过程:

逻辑地址到线性地址的转换过程

1. 逻辑地址的组成

段选择子(Segment Selector):指示具体使用哪个段。

段内偏移量(Offset within the segment):相对于段基址的偏移。

2. 段选择子的组成

索引:13位,指定段描述符表中的条目。

TI:1位,指示段描述符表的类型,全局描述符表GDT或局部描述符表LDT。

RPL:2位,请求的特权级别。

3. 段描述符

段描述符包含段的基址、段界限和段属性信息。段描述符表存储在内存中,由全局描述符表(GDT)和局部描述符表(LDT)组成。

4. 转换过程

段选择子:从段选择子中提取索引、TI和RPL。根据TI位确定是从GDT还是LDT中查找段描述符。

查找段描述符:使用索引从指定的段描述符表(GDT或LDT)中获取段描述符。

段描述符包含段基址(Base Address)、段界限(Limit)和段属性(Attributes)。

计算线性地址:线性地址 = 段基址 + 段内偏移量。如果段内偏移量超过段界限,则发生段错误(Segment Fault)。

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

在现代操作系统中,虚拟地址需要通过页式管理机制转换为物理地址。为了高效管理内存并减少页表所需的内存,现代操作系统采用多级页表结构。以32位系统为例,典型的两级页表结构如下:

页目录表:包含页目录项,每个页目录项指向一个页表。

页表:包含页表项,每个页表项指向一个物理页框。

地址转换过程

1. 虚拟地址分解

假设虚拟地址为0x12345678,页大小为4KB(12位偏移量),则虚拟地址分为三部分:

页目录索引:高10位,用于索引页目录表。

页表索引:中间10位,用于索引页表。

页内偏移:低12位,用于定位页框内的具体地址。

具体分解如下:

页目录索引:0x12345678的高10位 -> 0x48。

页表索引:0x12345678的中间10位 -> 0xD2。

页内偏移:0x12345678的低12位 -> 0x678。

2. 查找页目录表

使用页目录索引0x48从页目录表中查找对应的页目录项,假设页目录项的值为0x00123000,这是页表的基址。

3. 查找页表

使用页表索引0xD2从页表0x00123000中查找对应的页表项,假设页表项的值为0x98765000,这是物理页框的基址。

4. 计算物理地址

将物理页框的基址0x98765000与页内偏移0x678组合,得到物理地址:

物理地址 = 物理页框基址 + 页内偏移。

物理地址 = 0x98765000 + 0x678 = 0x98765678。

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

四级页表系统包含四个层次的页表:PML4、PDPT、页目录(PD),和页表(PT)。每一级页表通过将虚拟地址的一部分映射到物理地址的一部分,逐级解析出最终的物理地址。

虚拟地址的分解

PML4 Index(9位):用于索引 PML4 表项。

PDPT Index(9位):用于索引 PDPT 表项。

PD Index(9位):用于索引页目录表项。

PT Index(9位):用于索引页表表项。

Offset(12位):页内偏移量。

地址转换过程

PML4 查找:

使用 VA 的最高 9 位查找 PML4 表项。

获取到 PDPT 表的物理地址。

PDPT 查找:

使用 VA 的下一个 9 位查找 PDPT 表项。

获取到页目录(PD)表的物理地址。

页目录(PD)查找:

使用 VA 的下一个 9 位查找 PD 表项。

获取到页表(PT)的物理地址。

页表(PT)查找:

使用 VA 的下一个 9 位查找 PT 表项。

获取到实际物理页帧的物理地址。

计算物理地址:

使用 VA 的最后 12 位作为页内偏移量,加上页帧地址,计算出最终的物理地址(PA)。

TLB 是一个缓存,用于存储最近使用的 VA 到 PA 的映射。它加速了地址转换过程,减少了查找页表的开销。

TLB 查找:

在访问内存时,首先检查 TLB 是否有对应的 VA 到 PA 的映射。

如果找到(TLB 命中),直接使用 TLB 中的映射,转换完成。

TLB 未命中:

如果 TLB 中没有对应映射,则需要进行页表查找(如上所述的四级页表查找)。

将查找到的映射加载到 TLB 中,方便后续访问。

TLB 和页表的结合

快速路径:TLB 提供快速地址转换路径,减少每次内存访问的延迟。

慢速路径:当 TLB 未命中时,系统会依赖四级页表进行完整的地址解析。

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

现代高速缓存将m个地址华为了标记位,组索引位和块偏移位。整体的cache结构类似于一个表。其中每行第一位为标记位,然后是t个标记位,最后是2^b字节是存储空间。由E行构成一组,一共2^s组。

如果当前选中组有效位为1,且标记位和地址中的标记位相同,我们就认为得到了一个缓存命中,否则就成为一个缓存不命中。如果缓存失效,就需要从它的下一个层次去除需要的块,然后将新的块存在缓存行中。

L1 缓存(一级缓存):速度最快,容量最小,通常分为指令缓存(L1i)和数据缓存(L1d)。

L2 缓存(二级缓存):比 L1 缓存稍慢,但容量更大,通常是统一的(既存储指令也存储数据)。

L3 缓存(三级缓存):最慢但容量最大,通常在多核处理器中共享。

物理内存访问过程

当处理器需要访问数据时,访问路径通常如下:

检查 L1 缓存:

首先检查 L1 缓存中是否有所需数据。

如果 L1 缓存命中(数据在 L1 缓存中),直接返回数据。

如果未命中,进入下一层缓存(L2 缓存)。

检查 L2 缓存:

如果 L1 缓存未命中,检查 L2 缓存中是否有所需数据。

如果 L2 缓存命中,直接返回数据。

如果未命中,进入下一层缓存(L3 缓存)。

检查 L3 缓存:

如果 L2 缓存未命中,检查 L3 缓存中是否有所需数据。

如果 L3 缓存命中,直接返回数据。

如果未命中,进入主存(DRAM)。

访问主存:

如果 L3 缓存未命中,处理器访问主存中的数据。

将所需数据从主存加载到 L3 缓存中,然后逐级加载到 L2 和 L1 缓存中。

图68 存储器层次结构的示例

7.6 hello进程fork时的内存映射

内存映射概述

虚拟内存结构:现代操作系统使用虚拟内存,进程的地址空间分为多个段,如代码段、数据段、堆、栈等。每个进程有独立的虚拟地址空间,与物理内存通过页表映射。

fork 的基本流程:

创建子进程的内核对象。

复制父进程的页表,但不立即复制物理内存。

设置子进程的页表,使其虚拟地址空间与父进程一致。

使用写时复制机制。

写时复制

页表复制:fork 时,子进程会得到与父进程相同的页表副本,指向相同的物理页。

所有的内存页标记为只读,以便在写入时触发 CoW 机制。

写时复制机制:如果父或子进程试图写入某个页,操作系统会在写入发生前复制该页,确保两个进程各自有独立的物理页。

复制后的页解除只读限制,允许写入。

这种技术提高了 fork 的效率,因为只有在写入时才会进行实际的内存复制,从而减少了不必要的内存开销。

7.7 hello进程execve时的内存映射

execve 是一种用于执行不同程序的系统调用。当调用 execve 时,当前进程的地址空间会被新的可执行文件的地址空间替换。原来的代码、数据和堆栈段将被新的程序段覆盖,但进程的 PID 不变。

内存映射过程

清空当前地址空间:操作系统首先卸载当前进程的所有内存映射,包括代码段、数据段、堆、栈等。

加载新程序的可执行文件:操作系统读取新的可执行文件(如 ELF 文件)头部,解析其中的程序头表(Program Header Table)以确定各个段(如代码段、数据段、BSS 段等)的大小和位置。

建立新的内存映射:根据可执行文件的程序头表,操作系统为新的程序分配和映射内存区域。

具体步骤

解析可执行文件:操作系统读取新的可执行文件(通常是 ELF 文件)的头部,获取程序头表信息。

程序头表包含多个段(如 .text、.data、.bss)的起始地址、大小、权限等信息。

内存区域映射:代码段(.text):映射为可执行和只读。数据段(.data):映射为可读写。BSS 段(未初始化数据段):映射为可读写,并初始化为零。堆栈段:分配新的栈空间,映射为可读写。堆:初始时可能不分配,按需增长(通过 brk 或 mmap)。

加载动态库:如果新程序依赖动态链接库,操作系统会加载这些库,并建立必要的内存映射。

设置入口点:操作系统根据可执行文件的头部信息设置程序的入口点,将程序计数器(PC)指向该入口点。

初始化用户栈:操作系统将命令行参数(argv)和环境变量(envp)压入新的用户栈,并设置栈指针(SP)。

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

缺页故障触发条件

当程序访问一个未映射的虚拟地址时会触发缺页故障,这种情况可能有以下几种原因:该页面尚未加载到物理内存中(例如,延迟加载)。该页面被换出到磁盘的交换空间。该页面指向的虚拟地址无效(非法访问)。

缺页故障处理流程

触发缺页故障:CPU 发现所访问的虚拟地址不在当前页表的映射范围内,产生缺页故障并生成缺页异常。

保存状态:CPU 中断当前指令的执行,保存寄存器状态和程序计数器(PC),以便中断处理完成后能够恢复执行。

内核异常处理:转到操作系统内核态,执行缺页异常处理程序。

检查页表:内核检查页表项,确定缺页的原因,是合法访问还是非法访问。

如果是非法访问,操作系统会终止进程并产生错误信号(如 Segmentation Fault)。

页面调度:如果访问是合法的,操作系统需要将缺失的页面加载到物理内存中。

从磁盘或交换空间中读取页面到内存中。

更新页表:更新页表项,设置新页面的物理地址及相应的权限(读、写、执行)。

页表项更新后,将页面标记为已加载。

恢复进程执行:恢复先前保存的寄存器状态和程序计数器,重新执行引发缺页故障的指令。

此时,虚拟地址已经映射到物理内存,指令可以正常执行。

图69 缺页中断处理

7.9动态存储分配管理

动态存储分配的基本概念

堆内存:堆是一个用于动态内存分配的区域,程序可以在运行时请求和释放内存。

与栈内存不同,堆内存的生命周期由程序员控制。

内存分配与释放:

malloc:分配指定大小的内存块,不初始化。

calloc:分配指定大小的内存块,并初始化为零。

realloc:调整已分配内存块的大小,可以扩展或缩小。

free:释放之前分配的内存块。

动态存储分配算法

首次适配(First-Fit):从头开始搜索空闲列表,找到第一个足够大的空闲块进行分配。优点:简单,分配速度快。缺点:可能导致前面的空闲块被分割成小块,增加碎片。

最佳适配(Best-Fit):搜索整个空闲列表,找到最小的足够大的空闲块进行分配。优点:减少大块空闲内存的浪费。缺点:搜索时间长,容易产生小碎片。

最差适配(Worst-Fit):搜索整个空闲列表,找到最大的空闲块进行分配。优点:减缓空闲块变小的速度。缺点:可能导致大块内存浪费,搜索时间长。

分区适配(Buddy System):内存分为固定大小的块,按需求分割成更小的块。优点:减少外部碎片,合并简单。缺点:可能产生内部碎片。

动态存储分配管理的实现

空闲列表管理:空闲列表是一个数据结构(如链表或树)来跟踪未分配的内存块。分配内存时,从空闲列表中找到合适的块并移除。释放内存时,将块重新插入空闲列表。

块分割与合并:当分配内存块比请求的大小大时,分割内存块。当释放内存块时,尝试与相邻的空闲块合并,以减少碎片。

内存碎片管理:内部碎片:分配的块比请求的内存大,多余部分无法使用。外部碎片:空闲内存块分布不连续,无法满足大的内存请求。

7.10本章小结

本章主要介绍了hello程序的存储器地址空间,段式管理,页式管理等。

分析了hello进程fork时的内存映射,hello进程,execve时的内存映射,缺页故障和缺页中断处理。同时解释了TLB,四级页表和三级缓存对地址转换的作用。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

Linux的I/O设备管理方法涉及设备的分类、设备文件的创建、设备驱动程序的使用,以及系统调用和内核与硬件之间的交互。在Linux中,设备分为字符设备、块设备和网络设备。

字符设备:字符设备按字节进行I/O操作,如键盘、串口等。这些设备通常支持按字符处理的系统调用,如 read 和 write。

块设备:块设备按块进行I/O操作,如硬盘、USB存储设备等。这些设备通常支持随机访问和缓存等高级功能。

网络设备:网络设备用于网络通信,如以太网卡、Wi-Fi适配器等。这些设备通过套接字接口进行操作,不映射为设备文件。

8.2 简述Unix IO接口及其函数

Unix I/O 接口基于文件描述符(File Descriptor,FD)进行操作。每个打开的文件或设备都会分配一个文件描述符,文件描述符是一个非负整数,操作系统通过它来识别打开的文件。

文件操作函数:

int open(const char *pathname, int flags, mode_t mode):打开文件或创建文件。

int close(int fd):关闭文件。

ssize_t read(int fd, void *buf, size_t count):从文件中读取数据到缓冲区。

ssize_t write(int fd, const void *buf, size_t count):将数据从缓冲区写入文件。

off_t lseek(int fd, off_t offset, int whence):在文件中定位文件指针的位置。

int fcntl(int fd, int cmd, ...):对文件描述符进行控制操作。

int rename(const char *oldpath, const char *newpath):重命名文件。

int unlink(const char *pathname):删除文件名。

文件描述符操作函数:

int dup(int oldfd):复制文件描述符。

int dup2(int oldfd, int newfd):复制文件描述符。

设备操作函数:

int ioctl(int fd, unsigned long request, ...):进行设备控制操作,对特定设备进行设置和查询。

套接字操作函数:

int socket(int domain, int type, int protocol):创建套接字。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen):绑定地址或建立连接。

int listen(int sockfd, int backlog):监听套接字上的连接请求。

多路复用函数:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout):用于多路复用I/O操作,检查多个文件描述符是否就绪。

管道操作函数:

int pipe(int pipefd[2]):创建管道。

8.3 printf的实现分析

printf 函数接收一个格式字符串 fmt 和多个参数,按照 fmt 的格式输出匹配到的参数。

在底层实现中,printf 调用了两个外部函数:vsprintf 和 write。vsprintf 负责将格式化的字符串写入缓冲区,而 write 系统调用则将缓冲区中的内容写入输出设备。

图70 printf函数示意图

图71 Vsprintf函数示意图

vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

图72 write函数示意图

这里是给几个寄存器传递了几个参数,然后调用sys_call这个函数。

图73 sys_call函数示意图

这个函数的功能就是不断的打印出字符,直到遇到:'\0';采用直接写显存的方法显示字符串。

8.4 getchar的实现分析

getchar 是 C 标准库中的一个函数,用于从标准输入(通常是键盘)读取下一个字符。它的实现涉及从硬件层面的键盘中断处理,到系统调用层面的读取操作。

getchar 实现的步骤

调用 getchar 函数:getchar 函数是一个简单的输入函数,没有参数。它返回读取的字符,或在出错或到达文件末尾时返回 EOF。

底层调用 fgetc(stdin):在标准库实现中,getchar 通常是通过调用 fgetc(stdin) 来实现的。stdin 是一个 FILE 类型的指针,指向标准输入流。

调用 fgetc(FILE *stream):getc 是一个通用的文件输入函数,它从指定的文件流中读取一个字符。对于 stdin,fgetc 从标准输入中读取一个字符。

缓冲区机制:fgetc 使用缓冲区机制来提高效率。它一次从操作系统读取多个字符,存储在缓冲区中。每次调用 fgetc,它从缓冲区中返回一个字符,如果缓冲区为空,它会重新填充缓冲区。

底层系统调用:当缓冲区为空时,fgetc 需要从操作系统读取新的数据。

这通常通过 read 系统调用实现,该调用读取输入设备的数据(如键盘)。

错误处理和EOF检测:如果 fgetc 成功读取一个字符,它会返回该字符。如果遇到文件末尾或发生错误,它会返回 EOF,并设置 errno 来指示具体的错误。

8.5本章小结

Linux 操作系统提供了强大的 I/O 设备管理机制,支持多种 I/O 操作函数。这些函数通过统一的接口与底层硬件交互,确保了系统的稳定性和高效性。本章主要叙述了Linux的IO设备管理办法以及各个IO接口的实现原理。同时具体分析了printf和getchar函数的实现原理。

结论

Hello程序的编译和执行过程实际上就包括了几乎所有c语言程序的编译执行过程,也就是说通过学习了解hello不断转化编译的过程,也就对c语言的总体运行流程有了一个了解。

在可执行文件形成过程:

编写:使用高级语言编写.c文件。

预处理:将.c文件转化为.i文件,合并外部库调用。

编译:将.i文件转化为.s汇编文件。将其转变为汇编语言

汇编:将.s文件翻译为机器码,生成可重定位目标程序hello.o。

链接:将.o文件和动态链接库链接为可执行目标程序hello。

对可执行文件hello的执行过程:

在Shell中输入命令,创建子进程,并调用execve函数加载并运行hello。

CPU分配时间片,加载器设置程序入口点,hello开始执行。

MMU映射虚拟内存至物理内存,CPU访问内存。

动态内存分配根据需要申请内存。

信号处理函数处理异常和用户请求。

执行完成后,父进程回收子进程,内核删除进程数据结构,hello运行结束。

感想:现在计算机已经发展成了一套十分庞大且精密的系统,逐渐涵盖了越来越多的功能,并不断提升着效率和执行能力。而我们只有不断深入了解其运行内核,才能逐步学好计算机,用好计算机,为将来的计算机发展做出贡献。

附件

文件名

内容

Hello.c

源文件

Hello.i

预处理文件

Hello.s

汇编文件

Hello.o

可重定位目标文件

Hello

可执行文件

Hello.elf

Hello.o的elf文件

Helloexe.elf

Hello的elf文件

Hello.asm

Hello.o的反汇编代码

Helloexe.asm

Hello的反汇编代码

参考文献

[1]  shell编程基础(扩展篇:bash启动一个命令的原理)_bash原理-CSDN博客

[2]  本电子书信息 - 深入理解计算机系统(CSAPP) (gitbook.io)

[3]  [转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com)

[4]  读CSAPP(4) - 虚拟内存 – heisenbug blog (heisenbergv.github.io)

[5]  C语言编译-CSDN博客

[6]  C语言编译和链接详解(通俗易懂,深入本质)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值