哈工大计算机系统大作业

计算机系统

大作业

计算机科学与技术学院

2022年5月

摘  要

本文,从C语言程序hello的“一生”入手,从预编译,编译,汇编,链接,运行,结束。来加深对“计算机系统”课程的理解,在linux系统下演绎了程序从编写到运行的每一步计算机的操作,并在此过程中再次了解计算机各构件的相互连接和相互支持。

关键词:计算机系统,linux,C语言,地址,I/O,程序的生命周期;                            

目  录

第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:要运行Hello.c,首先要完成由C语言控制台应用到可执行文件的转变,其中要对Hello.c进行四步操作:预处理,编译,汇编,链接,从而生成可执行文件,再在shell中运行该可执行文件,shell为其分配进程空间。这个过程称为P2P。                  

020:shell使用execve函数运行hello程序,映射虚拟内存,并从程序入口开始载入物理内存,再进入main函数执行目标代码,此时CPU为运行的hello分配时间片执行逻辑控制流,并通过流水线机制运行该程序,在此过程中,计算机通过TLB、4级页表、3级Cache,Pagefile等机制加速hello程序的运行,程序结束后,shell父进程负责回收hello进程,内核删除相关的数据结构。这个过程从hello这个程序不存在开始(即为0),到被回收(即为0)的过程,称为020

1.2 环境与工具

1.2.1 硬件环境

X64 CPU;2.6GHz;16G RAM;512GHD Disk

1.2.2 软件环境

Windows10 64位;VirtualBox 11;Ubuntu 20.04 LTS

1.2.3 开发工具

Visual Studio 2021 64位;CodeBlocks 64位;vi/vim/gedit+gcc

1.3 中间结果

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

1.4 本章小结

本章主要简单介绍了hello的P2P,020过程,列出了本次实验的环境、中间结果。也列出了该篇论文完成所需要生成的一些中间文件,为后续实验提供了基本思路。


第2章 预处理

2.1 预处理的概念与作用

概念:预处理也称为预编译,它为编译做预备工作,主要进行代码文本的替换工作,用于处理#开头的指令,其中预处理器产生编译器的输出。经过预处理器处理的源程序会有所不同,在预处理阶段所进行的工作只是纯粹的替换和展开,没有任何计算功能。

作用:

  1. 将源文件中以”include”格式包含的文件复制到编译的源文件中。
  2. 用实际值替换用“#define”定义的字符串。
  3. 根据“#if”后面的条件决定需要编译的代码。

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

可以发由原本的C程序,数十行扩展成了3000+行,其中原本的C语言编码被放在了最后,预处理器把所有#include对应的头文件从路径/user/include/中调出写入到预处理过后的文件中,同时将所有宏定义填充。

2.4 本章小结

本章介绍了linux环境下对C语言程序进行预处理的命令,同时简要介绍了预处理的概念和作用,然后用简单的hello程序实际演示了从hello.c到hello.i的过程并结合具体代码对预处理结果进行了简单的分析。

第3章 编译

3.1 编译的概念与作用

概念:将高级语言翻译成汇编语言或机器语言的过程

作用:将高级语言变成汇编语言,并提示语法错误

        

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1

数据:

(1)常数(立即数):存储在.text文件中,使用时加入寄存器中

(2)局部变量:说明将源程序的i存在-4(%rbp)

3.3.

赋值:

,将i的初值赋值为0

3.3.3

类型转换:atoi函数将argv[]中的值转为整型

3.3.4

算数操作:

每次循环结束时,i++

3.3.5

关系操作:

将i<8,翻译成为cmpl $7 ,-4(%rbp)

将argc!=4变成cmpl  $4 , -20(%rbp)

3.3.6

数组:

由上图可以看出,argc存在-0x14(%rbp)中即(%edi),argv[]存在

-0x20(%rbp)中即(%rsi)

3.3.7

控制转移:

  1. 如果i<=7,则继续跳转到.L4
  2. 如果,argc!=4,则跳转到exit(1)

3.3.8函数操作

  1. main:

参数传递:argc和argv[]

函数调用:程序开始时调用

局部变量:i

函数返回:%rax = 0 (return 0)

  1. printf():

参数传递:argv[1]和argv[2]的首地址

函数调用:if和for中

局部变量:无

函数返回:无

  1. sleep():

参数传递:atoi(argv[3])

函数调用:无

局部变量:无

函数返回:无

3.4 本章小结

简要介绍了编译的概念和作用,然后使用hello程序实际演示了从hello.i到hello.s的过程并结合具体代码对编译结果进行了简单的分析,通过源程序与汇编语言程序的对比,简要说明了编译器是怎么处理C语言的各类操作:数据,赋值,类型转换,算数操作,关系操作,数组,控制转移,函数操作等


第4章 汇编

4.1 汇编的概念与作用

概念:汇编器(as)将.s汇编程序翻译成机器语言,把这些机器语言指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中。这个过程就叫做汇编。

作用:将汇编语言翻译成机器语言,因为机器语言是计算机能直接识别和执行的一种语言。

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

    

4.3.1  ELF头(ELF header) - 描述文件的主要特性

ELF头(ELF header)位于文件的开始位置,它的主要目的是定位文件的其 他部分。比较重要的成员有:e_ident(ELF文件幻数)、e_machine(比如可 执行文件ET_EXEC)、e_entry(程序入口虚拟地址)等等

4.3.2  程序头表(Program header table) - 列举了所有有效的段(segments)和他 们的属性(执行视图)。

程序头是一个结构的数组,每一个结构都表示一个段(segments)。在可执 行文件或者共享链接库中所有的节(sections)都被分为不同的几个段程序 头的索引地址(e_phoff)、段数量(e_phnum)、表项大小(e_phentsize)都是通 过 ELF头部信息获取的。

4.3.3  节头表(Section header table) - 包含对节(sections)的描述(链接视图)

一个ELF文件中到底有哪些具体的 sections,由包含在这个ELF文件中 的 section head table(SHT)决定。每个section描述了这个段的信息,比如每个 段的段名、段的长度、在文件中的偏移、读写权限及段的其它属性。

4.3.4 系统预订的固定section

4.4 Hello.o的结果解析

操作数:汇编语言是十进制数,而机器语言都是十六进制

控制转移:汇编语言是跳到某一个模块(.L4),机器语言是跳到相对位置<main + x>

函数操作:调用函数时,汇编语言直接用函数名调用,而机器语言仍是用相对位 置调用,再在下一行写出函数的名称

4.5 本章小结

本章介绍了hello 从hello.s 到hello.o 的汇编过程,分析了可重定位文件的结构和各个组成部分,以及它们的内容和功能;还查看了hello.o 的elf 格式,并使用objdump 得到反汇编代码与hello.s 进行比较,了解从汇编语言映射到机器语言汇编器需要实现的转换。在这个过程中,生成了重定位条目,为之后的链接中的重定位过程奠定了基础。


5链接

5.1 链接的概念与作用

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

作用:链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。

5.2 在Ubuntu下链接的命令

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

5.3.1 ELF Header

这是一个64位ELF文件;数据以2的补码、用小端法表示;是可重定位文件;机器类型X86-64;入口点地址是0x1100;程序头开始地址是64;节头部开始地址是14928字节;ELF头的大小是64字节;节头部的大小是64字节;
共313个节头,节头部的字符串表索引是30。

5.3.2  Section Header

给出了了各个节的信息,包括名字、类型、大小、对齐方式、读写权限等

5.3.3  Program Headers

 列举出有效的段以及他们的属性

5.4 hello的虚拟地址空间

   

可以看出程序的开始的地址,以及main开始的位置

5.5 链接的重定位过程分析

  1. 相对于hello.o而言,hello的反汇编添加了更多的节(.init等)
  2. Hello的反汇编引入了许多函数,所以在main中可以直接调用函数的地址,而hello只能调用函数名
  3. Hello反汇编中的虚拟地址已经确定,但是hello.o中还是0

重定位过程:

(1)重定位节和符号定义:在这一步中,链接器将所有相同类型的节合并为同一个类型的新的聚合节,然后,链接器将运行时内存地址赋给新的聚合节,赋给输出模块定义的每个节,体积赋给输出模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。

(2)重定位节中的符号引用:在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的地址,要执行这一步,链接器依赖于可冲定位目标模块中称为重定位条目的数据结构。

5.6 hello的执行流程

0000000000401000 <_init>

0000000000401020 <.plt>

0000000000401090 <puts@plt>

00000000004010a0 <printf@plt>

00000000004010b0 <getchar@plt>

00000000004010c0 <atoi@plt>

00000000004010d0 <exit@plt>

00000000004010e0 <sleep@plt>

00000000004010f0 <_start>

0000000000401120 <_dl_relocate_static_pie>

0000000000401130 <deregister_tm_clones>

0000000000401160 <register_tm_clones>

00000000004011a0 <__do_global_dtors_aux>

00000000004011d0 <frame_dummy>

00000000004011d6 <main>

0000000000401270 <__libc_csu_init>

00000000004012e0 <__libc_csu_fini>:

00000000004012e8 <_fini>

5.7 Hello的动态链接分析

之前:

之后:

  

5.8 本章小结

本章主要介绍了链接的概念及作用,并在linux下实际进行了链接操作,并对生成的hello可执行文件的ELF格式、虚拟地址空间、重定位过程、执行流程、动态链接等方面进行了详细分析。


6hello进程管理

6.1 进程的概念与作用

概念:进程是一个正在运行的程序的实例。系统中的每个程序都在某个进程的上下文中运行。上下文由程序正确运行所必需的状态组成。这种状态包括存储在内存中的代码和程序数据、堆栈、通用寄存器的内容、程序计数器、环境变量和文件描述符的集合。

功能:进程为应用程序提供了两种抽象,一种是独立的逻辑控制流,一种是私有地址空间。提高CPU执行效率,减少因程序等待造成的CPU空闲和其他计算机软硬件资源的浪费。

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

流程:

  1. 读取命令行的字符串
  2. 分割字符串,读取对应的命令
  3. 判断是否为内置命令,如果是则直接运行,不是则fork一个子进程运行
  4. 子进程用execve运行对应的命令
  5. 等待前台进程结束
  6. 返回读去阶段

6.3 Hello的fork进程创建过程

过程:

  1. ./hello 学号 姓名 秒数
  2. 分割命令行作为参数
  3. 发现不是内置命令,fork()一个子进程,子进程除PID外完全继承父进程
  4. 将参数填入
  5. 运行,结束后回到shell

6.4 Hello的execve过程

fork()之后,子进程调用execve()函数在当前子进程的上下文中加载一个新的程序,删除程序原有的虚拟栈空间等,并调用启动代码,加载器创建内存映像,在程序头部的带领下,加载器将可执行文件的片复制到代码段和数据段,接下来。加载器跳转到程序的入口点

6.5 Hello的进程执行

在hello运行过程中,若hello进程不被抢占,则正常执行;若被抢占,则进入内核模式,进行上下文切换,转入用户模式,调度其他进程。当hello执行到sleep()的时候,hello进程会被抢占,直到sleep()返回,hello进程重新被调度。

6.6 hello的异常与信号处理

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

 hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

乱按发现如果按了回车,之后的一字母被当做getchar()的字符读入,其他的原封不动到了命令行,如果没有回车,则无影响,等待getchar()。

6.7本章小结

了解了进程的概念与作用,了解了shell-bash的概念与作用,了解了hello的fork进程创建过程,hello的execve过程与进程执行,了解了hello的异常与信号处理


7hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:机器语言中用来指定一个指令或操作数的地址,由段和偏移量组成

线性地址:是逻辑地址到物理地址的中间层,由段地址加偏移地址组成如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。

虚拟地址:与线性地址相似。

物理地址:在计算机科学中,物理地址,也叫实地址、二进制地址,它是在地址总线上,以电子形式存在的,使得数据总线可以访问主存的某个特定存储单元的内存地址。在和虚拟内存的计算机中,物理地址这个术语多用于区分虚拟地址。尤其是在使用内存管理单元(MMU)转换内存地址的计算机中,虚拟和物理地址分别指在经MMU转换之前和之后的地址。在计算机网络中,物理地址有时又是MAC地址的同义词。这个地址实际上是用于数据链路层,而不是如它名字所指的物理层上的。

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

段式管理是从逻辑地址到线性地址的变换:

一个逻辑地址由两部分组成,段标识符、段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,后面3位包含一些硬件细节,索引号,是“段描述符”,段描述符具体地址描述了一个段。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,由8个字节组成。

给定一个逻辑地址[段选择符:段内偏移地址],转换过程如下:

1、首先根据段选择符判断当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。这样就得到了一个数组。

2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。

3、Base + offset就是要转换的线性地址。

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

页式管理是从线性地址到物理地址的变换:

Linux有一个的虚拟内存系统,其虚拟内存组织形式如下图。Linux将虚拟内存组织成一些区域(称为段)的集合,段之外的虚拟内存不存在因此不需要记录。内核为hello进程维护一个单独的任务结构即图中的task_struct,其中条目 mm指向一个mm_struct,它描述了虚拟内存的当前状态,pgd指向第一级页表的基地址(结合一个进程一串页表),mmap指向一个vm_area_struct的链表,每个vm_area_struct都维护者一个区域。

物理内存被划分为一小块一小块的帧。分配内存时,帧是分配时的最小单位,最少也要给一帧。在虚拟内存中,与帧对应的概念就是页。线性地址的表示方式是:前部分是虚拟页号后部分是虚拟页偏移。

CPU通过将逻辑地址转换为虚拟地址来访问主存,这个虚拟地址在访问主存前必须先转换成适当的物理地址。CPU通过内存管理单元(MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址。然后CPU会通过这个物理地址来访问物理内存。

页表就是一个页表条目(PTE)数组,虚拟地址空间中的每个页在页表中的一个固定偏移量处都有一个PTE。PTE是由一个有效位和一个n个字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置。

MMU利用虚拟页号(VPN)来在虚拟页表中选择合适的PTE,当找到合适的PTE之后,PTE中的物理页号(PPN)和虚拟页偏移量(VPO)就会组合形成物理地址。其中VPO与PPO相同,因为虚拟页大小和物理页大小相同,所需要的偏移量位数也就相同。此时,物理地址就通过物理页号先找到对应的物理页,然后再根据物理页偏移找到具体的字节:

1.如果有效位是0,PPN为NULL 则代表没有在虚拟内存空间中分配该内存;

2.如果是有效位0,PPN不为NULL,则代表在虚拟内存空间中分配了但是没有被缓存到物理内存中;

3.如果有效位是1则代表该内存已经缓存在了物理内存中,可以得到其物理页号PPN,与虚拟页偏移量共同构成物理地址PA。

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

LB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。下图展示了当TLB命中时所包括的步骤,所有的地址翻译步骤都是在芯片上的MMU中执行的,因此非常快。

第1步:CPU产生一个虚拟地址。

第2步和第3步:MMU从TLB中取出相应的PTE。

第4步:MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。

第5步:高速缓存/主存将所请求的数据字返回给CPU。当TLB不命中时,MMU必须从Ll缓存中取出相应的PTE。新取出的PTE存放在TLB中,可能会覆盖一个已经存在的条目。

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

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

CPU发送一条虚拟地址,随后 MMU按照上述操作获得了物理地址 PA。根据cache大小组数的要求,将 PA分为 CT,CS,CO。根据CS寻找到正确的组,比较每一个 cacheline是否标记位有效以及 CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次去 L2,L3,主存判断是否命中,当命中时,将数据传给 CPU同时更新各级 cache的 cacheline

7.6 hello进程fork时的内存映射

虚拟内存和内存映射解释fork函数如何为每个新进程提供私有的虚拟地址空间,在shell运行hello进程时,shell为hello进程创建虚拟内存创建当前进程的的mm_struct, vm_area_struct和页表的原样副本,两个进程中的每个页面都标记为只读两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存随后的写操作通过写时复制机制创建新页面。

7.7 hello进程execve时的内存映射

shell进程调用execve函数在当前进程中加载并运行新程序hello的步骤:

删除已存在的用户区域,创建新的区域结构私有的、写时复制,代码和初始化数据映射到.text和.data区(目标文件提供),.bss和栈堆映射到匿名文件 ,栈堆的初始长度0,共享对象由动态链接映射到本进程共享区域,设置PC,指向代码区域的入口点,Linux根据需要换入代码和数据页面。

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

在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页(page fault)。下图展示了在缺页之前我们的示例页表的状态。CPU引用了VP 3中的一个字,VP 3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4。如果 VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在主存中这一事实。

接下来,内核从磁盘复制VP 3到内存中的PP 3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP 3已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了。下图展示了在缺页之后我们的示例页表的状态。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。分配器将堆视为一组不同大小的块( block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

7.10本章小结

本章简要地介绍了hello进程的内存地址空间管理,并对intel的段式管理和页式管理做了介绍,对TLB与四级页表支持下的VA到PA的变换和三级Cache支持下的物理内存访问进行了介绍,进而结合hello进程对fork与execve从虚拟内存视角进行了分析。最后介绍了缺页故障与缺页中断处理和动态存储分配管理。


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

一个 Linux 文件是一个 m 字节的序列,所有的 I/O 设备(如网络、磁盘和终端)都被建模为文件。每个文件都有一个类型来标明他在系统中的角色:

普通文件包含任意文件;目录是包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录;套接字是用来与另一个进程进行跨网络通信的文件

设备管理:unix io接口

所有的 I/O 设备(如网络、磁盘和终端)都被建模为文件,而所有的输入输出都被当做相应的文件的读和写来执行,这种将设备映射为文件的方式,允许linux内核引出一个低级的,简单的应用接口称为Unix I/O

8.2 简述Unix IO接口及其函数

Unix IO接口:

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

(2)linux shell 创建的每个进程开始时都有三个打开的文件:标准输入 、标准输出和标准错误。

(3)改变文件位置:内核对于每个文件保持一个初始为0的文件位置,该位置标示从头部开始的文件的偏移量。程序可以通过seek函数,显示地修改此文件位置。

(4)读写文件:读操作是从当前文件位置开始,复制相应数量的字节到内存,写操作则是从内存读入相应数量的字节到当前文件位置,然后更新文件位置。

(5)关闭文件:一个应用程序完成对文件访问后,要求内核关闭相应文件。

Unix IO函数:

(1)打开文件:int open(char* filename,int flags,mode_t mode)将filename文件转为操作符,mode为访问权限

(2)关闭文件:int close(fd)关闭fd文件,返回操作结果

(3)读文件:ssize_t read(int fd,void *buf,size_t n)从fd文件复制至多n个字节到buf处

(4)写文件:ssize_t wirte(int fd,const void *buf,size_t n)从buf处复制至多n个字节给fd文件

8.3 printf的实现分析

先来看printf的实现函数:

C语言中,参数压栈的方向是从右往左。
    也就是说,当调用printf函数的适合,先是最右边的参数入栈。
    fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素

下面我们来看看下一句:
     i = vsprintf(buf, fmt, arg);再看这是什么函数

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

再调用write函数

调用sys_call函数

sys_call函数通过总线将字符串中的字节从寄存器复制到显卡的显存。显存存储ASCII字符码,字符显示驱动子程序通过ASCII码在字体库中查找点阵信息,将点阵信息存储在vram中。

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

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

8.4 getchar的实现分析

getchar由宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

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

8.5本章小结

复习了有关linux的I/O的知识,学习和了解了printf和getchar的底层实现原理

结论

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

  1. Hello先通过预编译,由.c文件扩展为.i文件
  2. 接着Hello是通过编译,从普通人能看懂的高级语言变成了有功底的人才能看懂一二的汇编语言(.s)
  3. Hello.s再经过汇编变成了hello.o文件,至此为止,文件就只有计算机和远古大神能读懂了,因为只有二进制01串
  4. 光有hello一个不够,它用到了许多库函数来实现,所以通过链接把他们糅合生成可执行文件hello.out
  5. 在shell中按照输入的格式打开hello.out,shell会fork()一个子进程,再用execve来开启hello这个进程,为hello分配内存空间
  6. 期间hello调用许多函数诸如getchar(),printf()等等
  7. 格式输入不正确时直接exit
  8. 格式正确时,运行完毕被shell回收,致此hello的一生结束


附件

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

​​​​​​​

hello.c:源代码

hello.i:预编译之后的文件

hello.s:编译之后的文件

hello.o:汇编之后的代码

hello.out:通过ld链接之后的文件

hello.elf:hello的elf文件

hello.txt:通过对hello.out反汇编生成的代码

helloo.txt:通过对hello.o反汇编生成的代码


参考文献

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

[2]  printf 函数实现的深入剖析.

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

[3] 逻辑地址、线性地址和物理地址之间的转换

逻辑地址、线性地址和物理地址之间的转换_孤独剑0001的博客-CSDN博客_逻辑地址转换为物理地址过程图

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值