程序人生-Hello’s P2P

摘  要

本文通过hello程序从预处理开始,汇编、编译、链接、进程管理、存储管理以及IO管理,逐层深入,详细的了解一个hello程序从.c文件开始到可执行程序演变的方法与过程,使课程所学内容理解更加深刻,掌握更加熟练。

关键词:预处理;编译;汇编;进程;链接;内存管理;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简介

1.1.1 P2P过程阐述

P2P全称Program to process,其中program是指编辑器下完成的Hello.c文件,process是指预处理器将Hello.c的头文件进行预处理,插入对应头文件内容,得到Hello.i文件,然后编译器ccl将其翻译为汇编文件Hello.s,然后汇编器将汇编语言翻译成机器指令代码,得到Hello.o文件,最后链接过程,链接器将Hello.o和其他预编译好的目标文件合并到一起并且完成引用的重定位,得到一个可执行文件Hello。

在Ubuntu的Shell中可以通过./Hello执行,Shell会调用fork( )函数创建子进程,并在子进程中加载该程序,自此,Hello.c文件变为一个进程。

1.1.2 020过程阐述

Shell为其execve映射虚拟内存,进入程序入口后程序开始加载物理内存,然后进入main函数执行目标代码,CPU为运行该进程分配时间片,程序完成后,父进程负责恢复Hello进程,内核删除相关数据,这就是020的过程

1.2 环境与工具

硬件环境:i7 10875H CPU,16G内存

软件环境:VirtualBox 6.1.18  Ubuntu20.04.2LTS

开发工具:VIM8.1.2269   Visual Studio 2022

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

Hello.i

预处理后文件

Hello.s

编译后的汇编文件

Hello.o

汇编之后的可重定位目标文件

Hello.elf

Hello.o的可重定位文件

Hello

链接后的可执行文件

1.4 本章小结

本章对Hello的一生进行了简要的介绍,介绍了P2P与020的整个过程,介绍了完成大作业所需要的工具与环境,以及Hello执行过程中生成的中间文件名称及作用

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理的概念

在程序设计领域里,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。

通常预处理由预处理器实现,其中ISO C/C++要求支持的包括#if、#ifdef...(条件编译)、#define(宏定义)、#include(源文件)。

2.1.2预处理的作用

因为预处理的存在,可以使在编写程序时,方便的调用头文件中的函数,提高编写效率的同时保证了程序的完整性,以及宏定义等便于后续对程序参数的修改。

2.2在Ubuntu下预处理的命令

命令为 cpp hello.c>hello.i

2.3 Hello的预处理结果解析

Hello.c文件共23行。

Hello.i文件共3060行,可见文本增加了三千行代码。

 

开始部分为对头文件stdlib.h、stdio.h、unistd.h的预处理后的结果。

 

Hello.c文件的原内容在Hello.i文件的最后。

2.4 本章小结

本章介绍了预处理的概念以及其作用,并对Hello.c文件进行预处理进行了实际的操作并了解了预处理后文件格式的变化与对应变化的作用,通过对Hello.i的查阅,我们初步了解了预处理在程序编译的准备工作中的重要作用。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

3.1.1编译的概念

编译是利用编译程序从源语言编写的源程序产生目标程序的过程,编译的过程分为五个阶段:词法分析、语法分析、语义检查与中间代码生成、代码优化、目标代码生成。

在检查指令和语法符合语法规则之后,将其翻译为等价的汇编代码

3.1.2编译的作用

翻译高级语言为更贴近机器的汇编语言。        

3.2 在Ubuntu下编译的命令

命令:gcc -s hello.i -o hello.s

 

3.3 Hello的编译结果解析

3.3.1 main函数传入参数部分

在Hello.c文件中main函数第一行代码,使对主函数传入参数argc大小的比较如下:

if(argc!=4){

在Hello.s文件中的对应的汇编代码段则为如下,第一个传入的参数argc存储于寄存器%edi,同理传入的第二个参数*argv[ ]存储于%rsi寄存器。

3.3.2 printf函数中输出部分字符串存储

在Hello.c文件中有两个printf函数输出两段字符串,这两段字符串则是储存在

 

.LC0与.LC1段中。

3.3.3局部变量的定义

在main函数中定义了一个局部变量i,这个变量在汇编代码中部分如下:

 

3.3.4 对局部变量的自加操作

 

在定义的i之后程序进入.L3阶段,进行判断,如果i的数值满足小于等于7的要求,则跳转至.L4内进行循环内内容的操作,其中addl $1,-4(%rbx),则为对i进行自加的操作过程,call printf、call sleep则为对函数调用的过程。按顺序运行每自加一次则进入.L3进行一次判断,之后如满足判断条件则跳转回循环结构,如不满足则直接结束循环。

3.3.5 如不满足argv!=4条件结果

在不满足跳转结构后,按顺序执行,向寄存器传入参数$1,之后调用exit函数结束main函数的执行。

3.4 本章小结

本章介绍了程序编译过程中Hello.i到Hello.s的过程,通过Hello.s与Hello.c两个文档的对比,了解了c代码如何转换为汇编代码,介绍了汇编代码如何实现循环,分支结果的实现,如何传参,判断等操作。

(第3章2分)


第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

汇编器将Hello.s翻译成机器语言指令,把指令打包成一种叫做可重定向目标程序的格式,并将结果保存在目标文件Hello.o中,Hello.o文件为一个二进制文件,包含的十七个字节是main函数指令编码,在文本编辑器中打开则为乱码。

4.1.2汇编的作用

将汇编代码转化成为机器可以执行的命令,每一个汇编语句对应一条机器指令

4.2 在Ubuntu下汇编的命令

命令:gcc hello.s -c -o hello.o

 

4.3 可重定位目标elf格式

4.3.1 readelf命令

命令:readelf -a hello.o>hello.elf

4.3.2 ELF表头

包含了系统信息,编码方式,ELF头大小,节大小和数量一系列信息

4.3.3节头目表

描述了.o文件中出现的各个节的类型,位置,所占空间大小等信息

4.3.4重定位节

各个段引用的外部符号等在链接是需要重定位对这些位置的地址进行修改,链接器会根据重定位节条目计算正确的地址

 

4.3.5符号表

.symtab存放在程序中定义和引用的函数和全局变量的信息

4.4 Hello.o的结果解析

4.4.1命令

命令:objdump -d -r hello.o  

4.4.2分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

Hello.s文件操作数为十进制,而Hello.o的反汇编代码中操作数为十六进制,说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

在控制转移上,Hello.s根据段名称进行跳转,而反汇编则根据虚拟地址进行跳转,在函数调用方面上,Hello.s语句为call+函数名,而反汇编文件则为call+目标虚拟地址。

4.5 本章小结

本章介绍了汇编操作中,将汇编语言转化为机器语言,Hello.s文件到Hello.o文件可重定位目标文件,通过readelf指令获取.elf格式文件,通过objdump获取Hello.o文件的反汇编文件,对汇编的过程有了较深入的理解

(第4章1分)


5链接

5.1 链接的概念与作用

5.1.1链接的概念

链接是将各种代码以及数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行

5.1.2链接的作用

链接令分离编译成为可能,方便程序的修改与编译,在重新编译时,仅编译修改的文件,之后重新链接使用,不需要啊重新编译其他文件。

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/libcso /usr/lib/x86_64-linux -gnu/crtn.o

 

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

命令:readelf -a hello  获取数据

Hello可执行文件ELF头信息

与Hello.o文件不同,Hello可执行文件的节表头数增加为27,且程序头变为56字节,有12个程序头。

 

在段头表中即可以获取各个段的具体信息

5.4 hello的虚拟地址空间

使用edb加载Hello文件,通过Data Dump窗口可以发现Hello程序的虚拟地址从0x00401000-0x00401ff0

程序包括PHDR、INTERP、LOAD、DYNAMIC、NOTE、GNU_STACK、GNU_RELRO几个部分。

其中PHDR保存程序头表,起始地址0x00401000偏移0x40处,大小0x2a0字节

INTERP:指定在程序已经从可执行文件映射到内存之后,必须调用的解释器,记录了程序所用ELF解析器的位置位于/lib64/ld-linux-x86-64.so.2

LOAD:表示一个需要从二进制文件映射到虚拟地址空间的段,其中保存了常量数据、程序的目标代码等。

DYNAMIC:保存了动态链接器使用的信息。

NOTE:保存辅助信息。

GNU_STACK:权限标志,标志栈是否可执行的

GNU_RELRO:表示这段在重定位结束后哪些内存需要设置为只读。

5.5 链接的重定位过程分析

命令:objdump -d -r hello>helloasm.s

5.5.1新增库函数

链接加入了Hello.c文件所调用的库函数printf,getchar等。

5.5.2控制流跳转地址

在Hello的反汇编代码中,已经完成重定位,因此在调用是已经是确切的虚拟地址

 

5.5.3链接的过程

链接就是链接器将各个目标文件组合到一起,文件中各个函数按照一定规则执行,从.o提供的重定位条目将函数调用和控制流跳转的地址填写为最终的地址

5.5.4 Hello的重定位过程

链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。重定位由两部组成。在hello到hello.o中,首先是重定位节和符号定义,链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。例如,来自所有的输入模块的.data节被全部合并成一个节,这个节成为hello的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。

5.6 hello的执行流程

 

 

5.7 Hello的动态链接分析

根据前文.elf文件获取程序头中.got.plt文件起始地址

 

在edb调试之后发现原来的global_offset表全为0的状态,在执行dl_init后被赋予了相应的偏移量,说明该操作为给程序赋予当前执行内存偏移量,是初始化hello程序的第一步。

5.8 本章小结

本章通过解释链接的概念与作用,分析可执行文件hello的elf格式文件,以及hello的重定位过程,执行流程,动态链接的过程,深入了解了链接的整个过程。

(第5章1分)


6hello进程管理

6.1 进程的概念与作用

6.1.1进程的概念

进程是一个执行中程序的实体,是系统进行资源分配和调度的基本单位,是操作系统结构的基础;在当代面向线程设计的计算机结构中,进程是线程的容器,程序是指令数据及其组织形式的描述,进程是程序的实体。

6.1.2进程的作用

提供一个独立的逻辑控制流,提供一个私有的地址空间

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

6.2.1Shell-bash作用

Shell执行读取/处理步骤,该步骤读取来自用户的一个命令行,处理解析命令行,之后执行对应操作

6.2.2处理流程

  1. shell打印一个提示符,等待用户输入命令行,然后对该命令行进行处理
  2. 解析输入命令行后,shell对其是否是内置命令进行判断,如是,则立即解释这个命令
  3. 如不为内置命令,那么shell建立一个子程序,在子程序中执行该程序,如用户要求后台运行,则shell返回循环顶部,等待下一个命令行,否则shell调用waitpid函数等待作业终止,终止后开始下一轮。

6.3 Hello的fork进程创建过程

父进程调用fork函数创建一个新运行的子进程,新创建的子进程与父进程几乎相同,子进程得到父进程用户及虚拟地址空间相同的一份副本,子进程还获取与父进程打开文件描述符相同的副本,这就意味着父进程调用子进程是,子进程可以读写父进程中打开的所有文件,二者区别为父进程与子进程有不同的PID。每调用一次fork在父进程中fork返回子进程的PID,子进程中的fork则会返回0,虽然二者拥有相同的独立地址空间,但是他们同时都具有自己的私有地址空间。

6.4 Hello的execve过程

子进程调用execve函数加载并调用程序,当其被调用时会在当前进程的上下文中加载并运行一个新程序,它被调用一次从不返回,具体过程如下:

  1. 删除已存在的用户区域
  2. 映射私有区:为hello的代码、数据、栈区域创建新的区域结构,所有这些区域都是私有的。
  3. 映射共享区:比如hello程序与共享库libc.so链接
  4. 设置PC:execve( )做的最后一件事就是设置当前进程上下文之中的程序计数器,使其指向代码区域的入口点
  5. execve( )在调用成功的情况下不会返回,只有当出现错误时,例如找不到要执行的程序时,才返回调用其的程序

6.5 Hello的进程执行

1.逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。

2.用户模式和内核模式:用户模式下进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据,内核模式进程可以执行指令集中任何命令,并且可以访问任意的内存位置。

3.上下文:上下文就是内核重启一个被抢占的进程所需要恢复的原来的状态

4.调度的进程:在对进程调度过程中,操作系统主要进行了加载保存的寄存器和切换虚拟地址空间 两个操作

5.用户态与核心态转换:为了能让处理器安全运行,划分用户态与核心态,核心态拥有最高访问权限,只有当进程故障、中断或者陷入系统调用才会获取内核访问权限,其他情况均处于用户权限下,保证系统安全性。

6.6 hello的异常与信号处理

6.6.1正常运行

 

6.6.2异常信号类型

 

6.6.3发送信号

Ctrl+Z:进程收到SIGSTP信号,挂起hello进程,查看hello进程PID为4321,在查看后台job序号为1

 

再利用fg 1将hello进程调回前台

Ctrl+C:进程收到SIGINT信号,结束hello进程,ps查看不到hello的PID

  

利用jobs查看后台进程也查询不到

  

中途乱按:只是将输入转入缓冲区。

 

kill指令:被挂起的进程终止,无法查看到其PID

 

6.7本章小结

本章了解了进程的概念与执行过程,深入了解了在hello的执行过程中,内核对其调度的过程,以及各种不同信号的不同的处理机制,对不同的shell收到的命令行有不同的对应的处理方式。

(第6章1分)


7hello的存储管理

7.1 hello的存储器地址空间

7.1.1逻辑地址

逻辑地址是指由程序产生的语段相关的偏移地址部分,在这里指hello.o中的内容

7.1.2线性地址

线性地址是指逻辑地址经过段机制后转化为线性地址,其地址是一个非负数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址,就是hello里虚拟内存地址

7.1.3虚拟地址

CPU启动保护膜时候,程序运行在虚拟地址空间中,保护模式下,hello运行在虚拟地址空间中,它访问存储器所用的逻辑地址。

7.1.4物理地址

它是在地址总线上,时数据总线可以访问贮存某个特定存储单元的内存地址

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

Intel平台下,逻辑地址的格式为段标识符:段内偏移量,段标识符是由一个16位长的字段组成,称为段选择符,其中前13位为一个索引号,后3位包含一些硬件细节,分段机制将逻辑地址转化为线性地址步骤如下:

  1. 使用段选择符中的偏移值在GDT或LDT表中定位相应的段描述符。
  2. 利用段选择符检验段的访问权限和范围,以确保该段可访问。
  3. 把段描述符中取到的段基地址加到偏移量上,最后形成一个线性地址。

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

页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。

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

7.4.1翻译后备缓冲器

在这个转换中需要用到翻译后备缓冲器(TLB),首先我们先将线性地址分为VPN(虚拟页号)+VPO(虚拟页偏移)的形式,然后将VPN拆分为TLBL(TLB索引)然后去TLB缓存里找所对应的PPN(物理页号),如果发生缺页情况则直接查找对应的PPN,找到PPN后,将其与VPO组合变为PPN+VPO就是生成的物理地址了。

7.4.2多级页表

将虚拟地址的VPN划分为相等大小的不同的部分,每部分用于寻找由上一级的页表基址对应的页表条目

 

7.4.3 VA到PA的变换

上图给出了Core i7 MMU 如何使用四级的页表来将虚拟地址翻译成物理地址。36位VPN 被划分成四个9 位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE 包含L2 页表的基地址。VPN 2 提供到一个L2 PTE 的偏移量,以此类推。

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

高速缓存(三级cache):高速缓存存储器是小型的,快速的基于SRAM的存储器是在硬件中自动管理的

缓存的出现是为了环节存储设备和CPU之间巨大的速度差异,处理器对内存数据访问,通产是通过cache进行的,具体过程为:通过地址解析出缓存的索引和偏移,对缓存进行访问,匹配标记查找是否含有相关的字,如果命中,则将数据发送给CPU,否则,则访问下一级缓存,取出这个字存入高一级缓存返回给CPU。

7.6 hello进程fork时的内存映射

当fork函数被shell调用时,内核位hello进程创建各种数据结构,并为其分配一个唯一的PID,为给hello创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制,当fork返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当父进程与子进程人一个后来进行写操作时,写时复制机制会创建新页面,因此,也就为每个进程保持了私有地址空间。

7.7 hello进程execve时的内存映射

execve函数在shell中加载并运行包含在可执行文件hello中的程序,用hello程序有效地替代了当前程序,加载并运行hello需要一下几个步骤:

  1. 删除已存在的用户区域,删除shell虚拟地址的用户部分中已存在的区域结构。
  2. 映射私有区域。为hello的代码、数据、bss和栈区域创建新的区域结构,这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
  3. 映射共享区域,如果hello程序和共享对象链接,比如标准c库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

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

7.8.1缺页故障

虚拟内存中的一个字存在于物理内存中,即缓存命中,缺页故障就是虚拟内存中的字不在物理内存中,发生页不命中。

7.8.2缺页中断处理

处理程序会选择一个牺牲页,则该页面被修改过,内核会从磁盘复制引发缺页异常的页面至物理内存中,更新页表随后返回,将控制转移给hello进程。再次执行触发缺页的虚拟地址重新发送到地址翻译硬件。

7.9动态存储分配管理

 动态储存分配管理使用动态内存分配器(如malloc)来进行,动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小块的集合,每个块就是一个连续的虚拟内存页,要么是已分配的要么是空闲的。

分配器有两种基本风格,即显式分配器和隐式分配器。显式分配器要求应用显式地释放任何已分配的块,具有以下约束条件:1.处理任意的请求序列。2.立即响应请求。3.只使用堆。4.对齐块。5.不修改已分配的块。隐式分配器要求分配器检测一个已分配块何时不再被程序使用时再释放这个块。

堆中内存块主要组织 为两种形式:

1.隐式空闲链表:一个块由一个字的头部,有效载荷,以及可能的填充组成。头部编码这个块的大小以及这个块是否已分配。头部后是应用malloc时请求的有效载荷,之后是一块大小任意的不使用的填充块。空闲块通过头部中大小字段隐含地连接着,称这种结构为隐式空闲链表,可以添加边界标记提高合并空闲块的速度。

2.显式空闲链表:在隐式空闲链表结构基础上,在每个空闲块中添加两个指针,分别指向前一个和后一个空闲块。

7.10本章小结

本章介绍了存储器地址空间、段式管理、页式管理、VA到PA的变换、物理内存的访问,hello进程fork和execve时的内存映射、缺页故障与缺页中断处理、包括隐式空闲链表和显式空闲链表的动态存储分配管理。

(第7章 2分)


8hello的IO管理

8.1 Linux的IO设备管理方法

文件是C语言和Linux管理的思想,所有的IO设备都被抽象为文件,所有的输入输出都作为对文件的操作。这个设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得输入和输出都能以一种统一且一致的方式的来执行。

8.2 简述Unix IO接口及其函数

Unix I/O接口:

  1. 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
  2. Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
  3. 改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K。
  4. 读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k。
  5. 关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

Unix I/O函数:

  1.  int open(char* filename,int flags,mode_t mode),进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
  2. int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
  3. ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
  4. ssize_t write(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

8.3 printf的实现分析

从printf函数可以看出,其中调用了vsprintf函数,按照格式fmt结合参数arg生成格式化的字符串,最后通过系统调用函数write输出,并返回字符串长度。

 

从代码可见,vsprintf主要作用为格式化,它接受一个格式字符串fmt来限制格式,将产生的输入写入buf中,返回要打印字符串的长度。

write函数调用

 

write函数通过寄存器进行传参,通过系统调用syscall实现输出的功能

sys_call函数

 

从代码分析可得,该函数打印字符,遇到’\0’停止。其中,[gs:edi]对应0x80000h:0,采用直接写显存的方式显示字符串。

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

8.4.1 getchar函数实现代码

 

函数主体中,getchar函数开辟静态输入缓冲区,如为空,则read函数读取字符,当程序调用getchar时,程序等待用户的键盘输入,用户输入的字符被存放在键盘缓冲区中。直到用户键入回车之后,getchar通过系统调用read从输入缓冲区每次读入一个字符。该函数返回用户输入的第一个字符的ASCII码,出错返回-1。

8.4.2异步异常-键盘中断的处理

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了Linux的I/O设备的基本概念和管理方法,以及Unix I/O接口及其函数,最后分析了printf函数和getchar函数的工作过程。展现了IO管理的强大功能

(第8章1分)

结论

Hello的一生:

预处理:将Hello.c调用的函数库加入Hello.i中,完成预编译处理

编译:将Hello.i编译为汇编文件Hello.s

汇编:将Hello.s文件变为可重定位目标文件Hello.o

链接:将Hello.o与可重定位目标文件和动态链接库连接成可执行文件Hello

运行:在shell中输入命令行./hello 120L02518 syx 1

子进程:调用fork( )函数创建子进程

运行流程:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。

执行流程:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流

动态内存申请:printf函数回调用malloc向动态内存分配器申请堆中的内存

信号:在程序执行中途输入Ctrl+C终止程序或Ctrl+Z暂时挂起程序。

在该大作业的完成过程中,逐渐深入理解了计算机底层的运行机制,对程序从编辑好源代码到变为可执行文件的完整过程有了深入的了解,同时也是对一学期内容的复习与回顾,使我受益匪浅。

(结论0分,缺失 -1分,根据内容酌情加分)


附件

列出所有的中间产物的文件名,并予以说明起作用。

Hello.c 源程序

Hello.i 预处理后的程序文本

Hello.s 编译为汇编后的程序文本

Hello.o 由Hello.s生成的二进制可重定位目标文件

Hello.elf Hello.o反汇编获得的汇编代码

Helloasm.s 由可执行文件Hello获取的反汇编代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值