哈工大计算机系统大作业——hello的一生

1 篇文章 0 订阅
1 篇文章 0 订阅

第1章 概述

1.1 Hello简介

P2P的: From Program to Process:

    Program是指编写源代码文件,Process是指内核为hello程序开辟进程,执行它。而从Program to Process中间其实经过了非常复杂的过程,首先是要对源程序进行预处理,针对预处理指令和特殊字符进行处理,然后编译器对预处理生成的文件进行编译,将高级语言语法编写的代码翻译成指令形式的汇编语言,汇编器再将其翻译成机器语言指令,并打包成可重定位目标文件(二进制文件),链接器将代码片段(如共享库等)和数据片段链接在一起,把需要重定位的节和地址定位具体的虚拟空间的地址生成可执行目标文件。在虚拟内存中开辟进程加载执行该可执行文件。

O2O: From Zero-0 to Zero-0:

运行hello的进程,内核为其分配虚拟地址空间,随着虚拟内存发生缺页故障和物理缓存中的冷不命中,hello载入内存。内核通过异常控制流对运行着的进程(hello进程)进行调度。当调用读写这样的io操作会通过Unix IO接口通过数据总线实现数据的读写和流的开关闭合。当程序终止,shell父进程将其回收,内核删除相关数据结构,hello一生的结束。

1.2 环境与工具

1.2.1 硬件环境

X64 CPU: R7-4800H ; 16G RAM ; 512GHD Disk

1.2.2 软件环境

Windows 10 64位  ; VMware 16 ; Ubuntu 20.04

1.2.3 开发工具

Visual Studio 2022 64 位;  Code Blocks 64位 ; vi/vim/gedit+gcc

1.3 中间结果

hello.i                                    预处理hello.c生成的文件

hello.s                             对hello.i 进行编译生成的文件

hello.o                                    对hello.s 进行汇编生成的文件

hello.elf                           对hello.o 进行readelf生成的文件

hello.asm                         对hello 进行objdump反汇编生成的文件

hello                                对hello.o进行链接生成的文件

hello.out.asm                   对hello进行objdump反汇编生成的文件

hello.out.elf                     对hello 进行readelf生成的文件

1.4 本章小结

       即使是程序员编写的最简单最开始的程序,也要经过复杂完善的机制才能在我们的计算机上执行与调度,本节简单叙述了hello的一生都经历了什么,和实验进行的环境。

第2章 预处理

2.1 预处理的概念与作用

C语言的预处理是由预处理程序(Cpp)负责完成。当我们将一个源文件编译成为可执行文件时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕再进行编译、汇编、链接最终成为可执行目标文件。

因此预处理阶段并不对源代码进行符号解析,而是对预处理命令(如将宏定义的对象展开等)和注释(将注释部分变为空格)等部分进行处理,为编译器聚焦命令解析做提前的准备和一些事项的处理。

作用:

一、对#命令(预处理命令)的处理

1、#include 指令告诉预处理器(cpp)读取源程序所引用的系统源文件,并把源文件直接插入程序文本中。

2、执行宏替换。将宏定义中的目标字符替换为我们所定义的字符。

3、条件编译。如(#if / #ifdef / #ifndef / #else / #endif等)

4、错误指令、行控制、杂注等

二、特殊符号,预编译程序可以识别一些特殊的符号,预编译程序对于在源程序中出现的这些串将用合适的值进行替换

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

1、文件本身大小巨幅增大

    文件大小由源代码527个字节变为64732个字节的预处理后的.i文件

2、可观察到注释的部分不见了(全部转为空白字符最后被丢掉了)

3、文件开头是库文件的拼接

4、#include <>/#include “” 预处理指令消失,预处理器读取对应头文件的内容,并直接插入程序文本中

5、中间部分是大量变量和函数的声明(主要是对外部库文件中函数的extren声明)

6、源代码中的main函数位于文件末尾,且没有被符号解析基本维持原样(空白字符丢掉了)

2.4 本章小结

       本章论述了预处理的概念和作用,预处理就是主要对预处理指令进行处理,为接下来进行编译做好准备,然后是对hello.c 预处理的结果进行分析。

第3章 编译

3.1 编译的概念与作用

概念:通过词法分析和高级语言本身语法构造要求等,编译器(如cc1)将高级程序语言编写的代码翻译成汇编语言构成的指令形式。

作用:

  • 源代码词法、语法解析

通过词法分析(扫描源代码将源代码的字符序列分割成一系列记号)、语法分析(基于词法分析分割成的一系列记号,生成语法树)等进行高级程序语言代码的翻译。编译程序的语法规则用上下文无关文法来刻画

二、代码优化

       通过对程序代码的等价变换,生成更有效地目标代码,提高性能与效率,例如gcc提供的Og、O1、O2等不同优化等级。

三、目标代码的生成与优化

目标代码生成器把中间代码变换成目标代码(汇编代码)。

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1 数据

a)常量:

       数值常量:

对于if(argc!=4) 像4这样的常量在汇编代码中

 将在栈中的argc与4进行比较,然后跳转

对于for(i=0;i<8;i++) 8这个常量在汇编代码中

可以看到编译器将i<8的比较转换成了i<=7。

字符串常量:

两次printf( )中的字符串都存储在.rodata节中,分别为.LC0 .LC1。  

b) 变量:

      

       Main函数的两个参数,argc在寄存器%rdi中存入栈中-20(%rbp)的位置

       Char *argv[] 在寄存器%rsi中存入栈中-32(%rbp)。

变量i存在栈中%rbp-4的位置上。

3.3.2 算术操作

i++这样的算术操作对于汇编代码 

3.3.3 关系操作与控制转移

a)

C代码中不等于(!=)4的判断对于汇编指令:

将4与argc比较,等于时(je)跳转.L2,不等于继续执行

b)

初始化i为0,传入-4(%rbp),无条件跳转.L3

c)

      

循环结束条件对i的比较,在汇编代码中如下所示

通过cmpl指令将i与常量7进行比较,如果小于等于(jle)就跳入循环体.L4,否则就直接继续进行:getchar()然后return 0。

3.3.4 数组操作

上文提到 char *argv[ ]存在栈中-32(%rbp)。argv数组中,argv[0]指向输入程序名称,argv[1]和argv[2]表示两个字符串。

-32(%rbp)是argv数组所在地址,argv数组存着char *,一个8字节,所以        addq  $16, %rax 之后%rax存着argv[2]对应所在地址,然后

movq  (%rax), %rdx 访问内存将 argv[2] 指向的字符串首地址存入%rdx

  

重复一遍类似操作将argv[1]存入%rsi

movq  -32(%rbp), %rax

addq   $24, %rax

movq  (%rax), %rax

movq  %rax, %rdi

将argv[3]传入%rdi

3.3.5 函数操作

a)printf 函数

1、

参数传递:

       接着上文,在调用printf函数前%rsi存着argv[1],%rdx存着argv[2],然后

 

       .LC1(%rip) 相对取址获得常量存着的字符串并传入%rdi中,

       %rdi是函数调用第一个参数,存放printf输出格式字符串,%rsi存放第二个参数,对应argv[1],%rdx存放第三个参数,对应argv[2].

函数调用:

2、

参数传递:直接将.LC0存储的字符串输出

函数调用:

      

b) main函数

int main(int argc,char *argv[])

参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储

函数调用:自动调用

局部变量:3.3.1中多说变量均为main函数局部变量

函数返回:向 %eax传入0返回,对应return 0。

c) exit函数

参数传递:传入1

函数调用:  

局部变量:无

函数返回:无

d)atoi函数:

参数传递:argv[3]为参数

函数调用:call     atoi@PLT

函数返回:该函数返回转换后的长整数,如果没有执行有效的转换,则返回零。

e) sleep函数

参数传递:将调用atoi函数的返回值作为参数

函数调用:call     atoi@PLT

函数返回:unsigned int 。若进程睡眠到参数所指定的时间则返回0,否则有信号中断则返回剩余秒数。

f)getchar函数

参数传递:无

函数调用:call     getchar@PLT

函数返回:一个char值

3.4 本章小结

本章介绍了编译的概念和作用。通过hello代码编译生成的汇编代码进行分析。介绍了汇编代码数据、算术操作、控制转移与函数调用等的实现。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器as将源代码编译成的汇编代码文件根据指令集翻译成机器语言指令,并打包成可重定位目标文件(二进制文件)

作用:机器语言指令打包生成的二进制文件才能被机器理解与执行其中的指令。

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.3.1 readelf指令读取elf格式

4.3.2  ELF头

以16字节序列开始,描述生成该文件系统字的大小和字节顺序,而后包含帮助连接器语法分析和解释目标文件的信息。然后是节头部表具体信息。

4.3.3 重定位节

需要链接时重定位的信息,链接器将对应位置需要修改对应位置为具体的地址信息。包含函数调用和rodata段存储的两个字符串常量的访问。

4.4.4 符号表

存放程序中定义的函数和全局变量的信息。

4.4 Hello.o的结果解析

objdump -d -r hello.o > hello.asm 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

机器语言与汇编指令:每条汇编指令都对应了一段16进制的机器指令这样才能被计算机理解与执行。

操作数:从汇编代码的十进制变为16进制操作数

分支转移:不在采取.L3 .L2这样的段名称这样的助记符,而是直接使用了相对地址。

函数调用:汇编代码文件直接调用函数名称,但hello.o的反汇编文件则是直接call了一段地址,但这个相对地址目前就是下一条指令即0,在后面留下了虚拟地址,需要在链接后才能变为确定的地址,对共享库的函数调用要在执行时才能被动态链接器确定。Leaq指令需要读取rodata段地址也采用了这种形式。

4.5 本章小结

本章介绍了汇编的概念与作用,通过对hello.o可重定位目标elf格式的基本分析观察elf文件的大体构成和对hello.o进行反汇编与hello.s进行对比观察汇编的作用。

5章 链接

5.1 链接的概念与作用

链接概念:将各种代码和数据片段收集并组合成一个单一可加载执行文件的过程。

作用:现代系统链接交给链接器来自动执行,而由于链接器的存在,使得可以进行分离编译,将巨大源代码文件分解成各个小块,可以独立修改编译各个模块,更改其中一个只需编译小模块染后重新链接,既方便管理各个子模块,又提高效率,减少耦合性。

5.2 在Ubuntu下链接的命令

 

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

5.3.1 ELF头

我们可以看到文件类型变成EXEC,可执行文件

可执行文件在虚拟内存的地址也不再是0,而是0x40开头的具体地址

5.3.2 节头部表

描述了各节的基本信息,包括名称,类型,大小,地址,旗标,偏移量,对齐等信息

5.4 hello的虚拟地址空间

      

可以看到根据4.3节头目标中.init段从0x4010000开始,与edb中一致

.text段从0x00401090开始。

5.5 链接的重定位过程分析

5.5.1 通过objdump命令获得反汇编信息:

5.5.2 新增了大量函数

增加了init函数、plt函数和原先所引用的函数的代码

5.5.3 增加分节

 

原先只有.text节

5.5.4 每段指令对应分配好的在虚拟内存中的地址

5.5.5 重定位原先函数调用和条件跳转的具体地址

上图中可见原先函数调用和例如leaq指令的重定向条目均定向为具体的虚拟内存中的地址。条件跳转的指令变为绝对地址。

5.6 hello的执行流程

可以看到main函数在0x4010c5  printf在0x401040  getchar在0x401050

exit在0x401070,sleep在0x401080

5.7 Hello的动态链接分析

通过hello.out.elf文件中节头表

看到.got.plt(实际上其本质是从.got表中拆除来的一部分,当开启延迟绑定(Lazy Binding)时,会将plt表中的长跳转(函数)的重定位信息单独放到此表中,以满足后续实际的延迟绑定)是在0x0004000位置

观察dl_init前后开启延迟绑定的该部分变化

之前:

之后:

5.8 本章小结

在本章中主要介绍了链接的概念与作用,分析了hello的ELF格式,在linux中使用edb等分析了hello的虚拟地址空间、重定位过程、执行流程和动态链接过程。

6章 hello进程管理

6.1 进程的概念与作用

概念:一个执行中程序的实例。系统中每个程序都运行在进程的上下文中。

作用:进程的概念为程序运行提供独占使用处理器内存等资源、逻辑流一条一条不断执行、独占内存空间的抽象。

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

Shell:Shell的作用是解释执行用户的命令,用户输入一条命令,Shell就解释执行一条,这种方式称为交互式,Shell还有一种执行命令的方式称为批处理(Batch),用户事先写一个Shell脚本(Script),其中有很多条命令,让Shell一次把这些命令执行完,而不必一条一条地敲命令。

处理流程:

1、用户输入一条命令

2、Shell程序读取从键盘输入的命令

3、判断命令是否正确,且将命令行的参数改造为execve()处理所需求的形式

4、fork出子进程,自身wait等待子进程结束

5、子进程调用execve运行指定的可执行文件

6、命令有后台进行提示符时,终端进程不等待子进程结束,直接输出提示,等待用户输入新的命令

6.3 Hello的fork进程创建过程

1、在bash中输入./hello 岳思源 120L021112 3

2、终端程序对命令进行解析,解析判断为执行一个程序

3、终端进程fork了一个子进程,子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码段、数据段、堆、共享库和用户栈。

4、子进程执行hello程序

5、内核调度父进程与子进程的并发,终端父进程等待子进程执行结束

6.4 Hello的execve过程

函数原型:

int execve(const char * filename, char * const argv[ ], char * const envp[ ]);

filename:要运行的程序所在的路径名称

argv:传递给程序运行的参数,数组指针argv必须以filename开头,NULL结尾

envp:传递给程序运行的环境变量,以NULL结尾。

该函数成功运行正确运行时不返回。逻辑控制流交给要运行的程序

6.5 Hello的进程执行

上下文信息:

是内核重新启动一个进程所需的状态,包含通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和内核数据结构如页表、进程表、已打开文件的文件表等。

用户模式与内核模式:

     用户模式的进程不允许执行特权指令,不允许直接引用地址空间中内核区的代码和数据。

内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何

内存位置。

进程调度的过程:

当内核调度到hello程序所在的进程时,进行上下文切换:

保存原先进程的上下文

恢复hello进程的上下文

将控制转移给 hello 进程

逻辑控制流实现从另一个进程到hello进程的切换

用户态与核心态的转换:

      当发生硬件中断、系统故障或者系统调用这样的异常进入核心态,内核自己调度进程时,也会进入内核模式进行上下文切换然后再进入用户模式。处理器用寄存器中的一个模式为实现这种功能。便于操作系统内核提供抽象和保护安全。

6.6 hello的异常与信号处理

按下Ctrl+Z:进程收到 SIGSTP 信号, hello 进程挂起并向父进程发送SIGCHLD。

运行ps命令:

可以看到hello进程

运行jobs命令:

能够看到shell程序中停止的作业

运行pstree命令:

运行fg命令:hello恢复前台执行

运行kill 命令:

      

可以看到hello程序被终止

按下Ctrl+C:向前台hello进程发送SIGINT信号,Hello进程被终止

6.7本章小结

本章介绍了进程的概念与作用,分析了hello程序的进程由shell创建,执行,通过异常控制流调度到结束回收等一系列过程。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:由程序产生的与段相关的偏移地址部分。在这里指的是hello.o中的内容。一个逻辑地址由两部份组成,段标识符和段内偏移量。

线性地址:是逻辑地址到物理地址变换之间的中间层。hello的代码产生的段中的偏移地址,加上相应段的基地址构成一个线性地址。

虚拟地址:就是在程序在虚拟内存中对应的位置,虚拟内存是现代操作系统为了方便管理主存和对进程提供私有内存空间抽象的一种对主存的抽象。我们之前看到hello程序在内存中的地址是0x40开头的,这就是一段虚拟地址对应虚拟内存中的位置。

物理地址: 是指出现CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。

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

机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过MMU(CPU中的内存管理单元)转换成物理地址才能够被访问。

在x86保护模式下,段的信息(段基线性地址、长度、权限等)即段描述符占8个字节,段信息无法直接存放在段寄存器中(段寄存器只有2字节)。Intel的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值(index)即段选择符。

在CPU中,跟段有关的CPU寄存器一共有6个:cs,ss,ds,es,fs,gs,它们保存段选择符。而同时这六个寄存器每个都有一个对应的非编程寄存器,保存的对应段描述符。

段描述符就是保存在全局描述符表或者局部描述符表中,当某个段寄存器试图通过自己的段选择符获取对于的段描述符时,会将获取到的段描述符放到自己的非编程寄存器中,这样就不用每次访问段都要跑到内存中的段描述符表中获取。

包括数据段描述符、代码段描述符等。

分段机制将逻辑地址转化为线性地址的步骤:

1)使用段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符.(仅当一个新的段选择符加载到段寄存器中是才需要这一步)

2)利用段选择符检验段的访问权限和范围,以确保该段可访问。

3)把段描述符中取到的段基地址加到偏移量(也就是上述汇编语言汇中直接出现的操作地址)上,最后形成一个线性地址。

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

Linux中逻辑地址等于线性地址。为什么这么说呢?因为Linux所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从 0x00000000 开始,长度4G,这样线性地址=逻辑地址+ 0x00000000,也就是说逻辑地址等于线性地址了。

CPU发出的地址将被MMU捕获,即为虚拟地址(VA),而MMU将这个地址翻译成物理地址。

Linux采用了分页的方式来记录对应关系,以更大尺寸的单位页来管理内存。在Linux中,通常每页大小为4KB。虚拟内存系统通过页表这种存在物理内存中的数据结构和MMU确定一个虚拟页是否缓存在DRAM中,如果是,对应的物理地址是什么,不是的话在磁盘的哪个位置。

页表是PTE(页表条目)的数组。每个PTE保存一个虚拟页的有效位和物理页号或磁盘地址。

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

7.4.1  TLB(翻译后备缓冲器):

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目)。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降到1或2个周期。许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)

7.4.2 多级页表:

将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。这样能够是单个页表在较小的占用小却实现对空间很大的虚拟内存的抽象。

7.4.3 VA到PA的转换

MMU通过虚拟地址生成一个在页表中的索引,在页表中对应PTE的有效位如果设置了,那么通过PTE中的物理内存地址和虚拟地址中的虚拟页号偏移量构造出物理地址,去对应内存的物理地址出取出内容。如果不命中,就在磁盘中,产生缺页故障,根据PTE中存放磁盘地址进行页面修复。

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

将翻译后的物理地址在当前存储器层次结构中寻找,从cpu寄存器到L1 cache,L2 cache ,L3 cache,内存一层一层寻找,前一层是后一层的缓存,当本层中不命中时,便向下递归寻找。物理地址分为 CT(标记)+CI(索引)+CO(偏移量),CI是缓存中的组索引,根据CI找到对应的组,根据CT判断是否命中,命中后根据CO块中偏移量在对应存储块中找到要取的数据,没有命中去下一层寻找。

7.6 hello进程fork时的内存映射

当终端进程调用fork时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

7.7 hello进程execve时的内存映射

1、 输入 ./hello yuesiyuan 120L021112 3

2、 execve加载hello程序后,设置栈,将控制传递给hello程序的主函数。

3、 删除已存在的用户区域

4、 映射新的私有区域。代码和初始化数据映射到.text和.data区(执行可执行文件提供),.bss映射到匿名文件,共享对象由动态链接映射到本进程共享区域,设置PC,指向代码区域的入口点。栈中从栈底到栈顶是参数和环境字符串,再往上是指针数组,每个指针指向刚才的环境变量和参数字符串。栈顶是系统启动函数libc_start_main的栈帧和预留的未来函数的栈帧。

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

缺页故障:虚拟内存在DRAM缓存不命中即为缺页故障。

缺页中断处理:发生缺页故障时启动缺页处理程序

1、缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。

2、缺页处理程序页面调入新的页面,并更新内存中的PTE

3、缺页处理程序返回到原来的进程,再次执行导致缺页的命令。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆,向上生长,对于每个进程,内核维护着一个变量brk(读做“break”),它指向堆的顶部。分配器将堆视为一组不同大小的块的集合。每个块就是一个连续的虚拟内存页,要么是已分配的,要么是空闲的。

已分配的块显式地保留为供应用程序使用。

空闲块保持空闲,直到它显式地被应用所分配。

一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。动态内存分配主要有两种基本方法与策略:

带边界标签的隐式空闲链表

显示空间链表

7.10本章小结

本章介绍了存储管理的概念、四种地址、段式管理、页式管理,虚拟内存地址到物理地址的转换流程和存储器层次结构下的物理内存访问,进程的内存映射、缺页故障与处理和动态存储的分配管理。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

一个 Linux 文件是一个 m 字节的序列,所有的 I/O 设备(如网络、磁盘和终端)都被模型化为文件,而所有的输入输出方式都被当做相应文件的读和写来执行。将设备都映射为文件,允许Linux内核引出简单低级的应用接口——UNIX I、O。

设备管理:unix io接口

将设备都映射为文件,允许Linux内核引出简单低级的应用接口——UNIX I、O。

8.2 简述Unix IO接口及其函数

8.2.1 打开文件:

一个应用程序要求内核打开相应文件,来访问I/O设备。会返回一个非负整数,称为描述符,他在后面的操作中标志这个文件,内核记录有关打开文件的所有信息,程序只需要记住该描述符。

8.2.2  Linux Shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。

8.2.3  改变当前文件的位置:对于每个打开的文件,内核保持着一个文件位置k,初始化为0。这个文件位置是从开头起始的字节偏移量,应用程序可以执行seek操作显示设置文件当前位置k。

8.2.4  读写文件:

读操作是从当前文件位置开始,复制n个字节到内存从打开位置k开始,k到k+n。写操作则是从内存读入n个字节到当前文件位置,从k开始然后更新文件位置k。

8.2.5  关闭文件:

内核释放文件打开时创建的数据结构,并将描述符恢复到可用的描述符池中,无论一个进程以何种原因终止时,内核都会关闭所有打开的文件,并且释放他们的内存资源。

8.3 printf的实现分析

int printf(const char *fmt, ...)
{
    int i;
    char buf[256];
    va_list arg = (va_list)((char*)(&fmt) + 4);
     i = vsprintf(buf, fmt, arg);
     write(buf, i);
     return i;
    }

其中的: (char*)(&fmt) + 4) 表示的是fmt之后中的第一个参数。

Va_list就是char *类型。

接着看i是vsprintf函数的返回值。

int vsprintf(char *buf, const char *fmt, va_list args)
   {
    char* p;
    char tmp[256];
    va_list p_next_arg = args;
    for (p=buf;*fmt;fmt++) {
    if (*fmt != '%') {
    *p++ = *fmt;
    continue;
    }
    fmt++;
    switch (*fmt) {
    case 'x':
    itoa(tmp, *((int*)p_next_arg));
    strcpy(p, tmp);
    p_next_arg += 4;
    p += strlen(tmp);
    break;
    case 's':
    break;
    default:
    break;
    }
    }
    return (p - buf);
   }

vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。返回要打印的字符串的长度,然后进行系统函数write调用打印i长度的字符串。

write:
     mov eax, _NR_write
     mov ebx, [esp + 4]
     mov ecx, [esp + 8]
     int INT_VECTOR_SYS_CALL

write 向寄存器传入数值,然后设置中断门,然后通过系统来调用syscall

sys_call:

     call save

     push dword [p_proc_ready]

     sti

     push ecx

     push ebx

     call [sys_call_table + eax * 4]

     add esp, 4 * 3

     mov [esi + EAXREG - P_STACKBASE], eax

     cli

     ret

syscall将字符直到’\0’的ascii码值通过总线传到显示芯片的存储空间中。

字符显示驱动子程序通过ASCII码在字体库中查找点阵信息,将点阵信息存储在vram中。

显示芯片按照刷新频率逐行读取vram,并通过信号线向显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

当程序调用getchar时,等待键盘输入硬件中断。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。直到用户键入回车后,getchar开始读取stdin缓冲区。

getchar调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了linux IO设备管理方法和Unix IO接口,理解Linux一切皆文件的思想并深入分析了printf的实现与getchar的实现。

结论

1、hello.c源文件经过预处理生成hello.i

2、编译器(cpp)编译hello.i生成hello.s汇编文本文件

3、汇编器汇编hello.s生成可重定位目标文件hello.o

4、链接器链接hello.o和他引用的动态链接库中的代码等生成可执行文件hello

5、shell中输入./hello yuesiyuan 120L021112 3 父进程调用fork,生成子进程

6、子进程调用execve函数加载并运行程序hello通过传递的参数,操作系统为这个进程分配虚拟内存空间

7、内核通过异常控制流调度hello进程

8、hello调用printf函数和getchar等函数进行IO

9、hello最终被父进程回收

麻雀虽小五脏俱全,hello的一生经过完整的计算机处理的流程,在屏幕上短短的一行输出却经由无数层抽象。

附件

hello.i                                    预处理hello.c生成的文件

hello.s                             对hello.i 进行编译生成的文件

hello.o                                    对hello.s 进行汇编生成的文件

hello.elf                           对hello.o 进行readelf生成的文件

hello.asm                         对hello 进行objdump反汇编生成的文件

hello                                对hello.o进行链接生成的文件

hello.out.asm                   对hello进行objdump反汇编生成的文件

hello.out.elf                     对hello 进行readelf生成的文件

参考文献

[1] 逻辑地址、线性地址和物理地址的转换_octopusHu的博客-CSDN博客_分段逻辑地址转物理地址

[2] https://docs.huihoo.com/c/linux-c-programming/

[3] 逻辑地址、线性地址和物理地址的转换_octopusHu的博客-CSDN博客_分段逻辑地址转物理地址

[4] [转]printf 函数实现的深入剖析 - Pianistx - 博客园

[5] Randal E. Bryant, David R. O'Hallaon. 深入理解计算机系统. 第三版. 北京市:机械工业出版社[M]. 2018: 1-737

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 你好!恭喜你开始学习Python!下面是Python中的传统开场白: ```python print("Hello World!") ``` 以上代码会输出一个简单的“Hello World!”消息。希望这能为你提供一个好的开始,祝你学习愉快! ### 回答2: Python是一种非常受欢迎的编程语言,它的语法简单、清晰,适合初学者入门学习编程。Hello World是Python编程中最基本也是最经典的入门示例,它是一个最简单的程序,只需要一行代码即可打印出“Hello World”。 Python中的print()函数可以输出一段文本,你可以在括号中写下你想要输出的内容。在这里,我们要输出的是“Hello World”。 在Python的IDE中,我们可以像下面这样写下第一行Python代码: ``` print("Hello World") ``` 在这行代码中,print是Python中的一个内置函数,它可以输出指定的内容。括号中的双引号是用来包含输出的文本的,它告诉Python这是一个字符串。当我们运行这段代码后,Python就会在控制台输出“Hello World”的文本。 除了在IDE中写入代码,我们还可以使用Python的命令行来输出Hello World。我们只需要打开命令行模式,键入python命令就可以打开Python解释器。然后,我们可以键入第一行代码print("Hello World"),按回车键,Python就会立即输出Hello World。 在初次体验Python的过程中,这个Hello World程序可以帮助我们了解程序如何工作,如何指定输出文本并将其打印到屏幕上。它也是我们编写更复杂的程序的基础和基石。无论你是计算机科学的专业人士还是仅仅是对编程感兴趣,学习Python编程都是一个不错的选择。 ### 回答3: Python初体验——Hello World Python是一门高级编程语言,而“Hello World”则是编程里的常见入门示例。让我们开始我们的Python初体验吧! 首先,我们需要安装Python解释器。Python解释器是一个解释执行Python代码的程序。Windows系统下可以在Python官网下载,并安装在本地计算机上。另外,Python也可以在很多云平台上使用,例如Google Colaboratory和Jupyter Notebook。 在安装完Python解释器之后,我们可以使用一些简单的文本编辑器,例如VS Code和Atom,在Python中运行代码。但是如果您是刚入门的新手,可以使用IDLE,IDLE是Python自带的集成开发环境,可以方便地输入、编辑、运行Python代码和在线调试。在Windows上,可以在开始菜单中找到它,然后输入“idle”或“Python”即可。 接下来,让我们通过经典的“Hello World”程序来进行Python的初体验。 打开IDLE编辑器之后,输入以下代码: print("Hello, World!") 该代码的含义是,输出一条带有“Hello,World!”的提示信息。我们点击“运行”(或按F5)即可看到结果。 也可以在Python交互模式下作,例如在命令行中键入“python”命令。随后,我们直接输入“print("Hello, World!")”即可看到输出。 Python还有许多其他的示例和功能,例如计算器和猜数字游戏等等。学习Python,你可以使用它来构建自己的小工具,理数据、创建网站,以及编写各种有趣的程序。 总之,Python是一门入门门槛较低,功能强大的高级编程语言,非常适合初学者入门。通过“Hello World”程序,我们可以体验到它的简洁、易学、高效,也能够更好地了解Python编程的本质。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值