Hello’s P2P

文章目录

第1章 概述

1.1 Hello简介

P2P过程:

  1. 预处理阶段:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c的第一行#include<stdio.h>命令告诉cpp读取系统头文件stdio.h的内容,并直接插入到文本之中,得到了另一个C程序,通常以.i作为文件扩展名。
  2. 编译阶段:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
  3. 汇编阶段:汇编器(as)将hello.s翻译成机器语言指令,并打包成可重定位目标程序的格式,将结果保存在hello.o中。
  4. 链接器(ld)负责链接hello.o与其他的一些库函数,比如printf函数,最终得到hello可执行目标文件,它可以被加载到内存中,由系统执行。
    020过程:
    通过在shell中输入./hello执行目标程序,shell会先用fork函数生成一个子进程,子进程通过execve()函数加载hello。操作系统为hello产生了一块VM,调度器为给hello划分时间片,然后cpu逐条读取hello中的指令。hello中又调用syscall系统调用变成内核态,调用write函数在屏幕上打印出结果。内核又将控制传回给hello,hello执行完毕之后,由shell回收了这个僵死的子进程。

1.2 环境与工具

硬件环境:Intel i7-6500U CPU; 12GB RAM; 256GB SSD
软件环境:VMware Workstation 15 Player;Ubuntu 18.04 64位;Windows 10 64
开发工具:Visual Studio 2019;GCC;EDB;objdump;readelf

1.3 中间结果

中间结果作用
hello.i预处理得到的文件
hello.s汇编语言文件
hello.o可重定位目标文件
hello可执行目标文件
elf1.txthello.o的elf文件
hellooobj.txthello.o的反汇编文件
elf2.txthello的elf文件
helloobj.txthello的反汇编文件

1.4 本章小结

本章介绍了hello的P2P与020过程,还有实验的环境以及一些中间产物。

第2章 预处理

2.1 预处理的概念与作用

预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c的第一行#include<stdio.h>命令告诉cpp读取系统头文件stdio.h的内容,并直接插入到文本之中,得到了另一个C程序,通常以.i作为文件扩展名。
预处理主要是处理宏定义、include包含、条件编译。

2.2在Ubuntu下预处理的命令

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


图2-1 预处理指令截图

2.3 Hello的预处理结果解析

打开hello.i发现文件变得很长,阅读hello.i文件发现他插入了大量#include文件中的代码。
在这里插入图片描述
图2-2(a) hello.i文件截图
文件结尾部分发现是原来的hello.c中的文件(没有了#include)。
在这里插入图片描述
图2-2(a) hello.i文件截图

2.4 本章小结

本章描述了预处理的概念与作用,文件在预处理的指令以及预处理生成文件的解释,大致的写出了预处理的过程。

第3章 编译

3.1 编译的概念与作用

编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
编译阶段主要完成词法分析、语法分析、语义分析等等,在检查无误之后,代码将被翻译成汇编语言。

3.2 在Ubuntu下编译的命令

gcc -m64 -Og -no-pie -fno-PIC hello.i -o hello.s -S

在这里插入图片描述
图3-1 编译指令截图

3.3 Hello的编译结果解析

在这里插入图片描述
图3-2(a) hello.s截图1
在这里插入图片描述
图3-2(b) hello.s截图2

3.3.1 字符串常量

在这里插入图片描述
图3-3 字符串常量截图
可以看到,字符串常量存在.rodata区,并且汉字是使用UTF-8编码的。

3.3.2 局部变量

在这里插入图片描述
图3-4 局部变量i截图
从上图可以看出,main函数把局部变量i放进了寄存器ebx,且把他赋初值为0(也就是执行了for循环里的初始化i=0),然后再在.L2中与7相比较。

3.3.3 算数操作

在这里插入图片描述
图3-5 i++实现
从图中可以看到,机器使用addl指令对ebx(局部变量i)完成了for循环里的i++操作。

3.3.4 关系操作

在这里插入图片描述
图3-6 i<8的实现
通过cmpl指令与jle指令完成了for循环中的i<8的操作,jle是小于等于,即是说i-7<=0,其意义对整数来说与i<8是相同的。

3.3.5 数组操作

在这里插入图片描述
图3-7 对argv数组的操作
从上图可以看出,现将数组argv首地址放进了rbp,然后对数组的操作是通过movq rbp+n完成的,由于一个argv数组里的元素占8个字节(指针),所以采用rbp加的均是8的倍数比如16,8,24,分别访问的是argv[2], argv[1] , argv[3]。

3.3.6 控制转移

在这里插入图片描述
图3-8 if操作
hello中出现了if的控制转移,采用上图所示汇编实现。argc在edi中,把edi与常数4相比较,如果等于4就跳到.L6,否则继续执行下面跟着的指令。

3.3.7 函数操作

在这里插入图片描述
图3-9 函数调用
如图所示,程序分别调用了两次printf,一次exit,atoi,sleep,getchar函数,都是通过先把参数传给rdi(或edi)、rsi(或esi)等传递参数的寄存器,然后再使用call指令跳到函数中去,完成函数的调用,并且返回值是保存在rax当中的。

3.4 本章小结

本章讲述了hello.i文件是怎么转换成汇编语言文件hello.s的。通过阅读汇编文件hello.s,可以发现C语言当中的一系列数据和操作的实现方法。

第4章 汇编

4.1 汇编的概念与作用

汇编阶段:汇编器(as)将hello.s翻译成机器语言指令,并打包成可重定位目标程序的格式,将结果保存在hello.o中。

4.2 在Ubuntu下汇编的命令

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

在这里插入图片描述
图4-1 汇编命令截图

4.3 可重定位目标elf格式

在这里插入图片描述
图4-2(a) ELF头
在这里插入图片描述
图4-2(b) 各节头
在这里插入图片描述
图4-2(c) 重定位节和符号表
从图中可以看出,hello.o的ELF文件里有着ELF头+.text+.rela.text+.data+.bss+.rodata.str1.8+.rodata.str1.1+.comment+.note.GNU-stack+.eh_frame+.rela.eh_frame+.symtab+.strtab+.shstrtab各节的节头。而重定位节当中储存着程序用到的函数和一些全局符号(例如puts,exit等等)。

4.4 Hello.o的结果解析

在这里插入图片描述
图4-3 objdump反汇编生成文件
从图中可以看到,机器语言是由操作码加一个数组成的,这个数可以是地址,相对地址,也可以是立即数。机器语言当中call是e8,但每个e8后面都跟着00 00 00 00,而汇编语言确实给出了要call的函数相对于main函数的偏移地址,这是一个很大的不同(由于链接器还没有工作)。而与hello.s中的汇编语言对比来看,hello.s中存在的.L1,.L2,.L3等等东西是不见了的,跳转时对.Ln的调用在反汇编中变成了jump到一个相对于main函数偏移多少的地方。
在这里插入图片描述
图4-4(a) 反汇编中的跳转
在这里插入图片描述
图4-4(b) 汇编中的跳转

4.5 本章小结

本章讲述了hello.s是如何转换成可重定位目标文件hello.o的,对hello.o与hello.s之间的不同点也进行了比较。并且运用了objdump和readelf等工具对hello.o文件进行了更加深入的分析。

第5章 链接

5.1 链接的概念与作用

链接器(ld)负责链接hello.o与其他的一些库函数,比如printf函数,最终得到hello可执行目标文件,它可以被加载到内存中,由系统执行。

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

在这里插入图片描述
图5-1 链接命令截图

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

在这里插入图片描述
图5-2(a)hello的ELF格式
在这里插入图片描述
图5-2(b)hello的ELF格式
从上面两图可以看到hello中各节的信息,节头部表分别反映了各段的名字、类型、地址、偏移量、大小、flag、链接信息、对齐大小等信息。

5.4 hello的虚拟地址空间

在这里插入图片描述
图5-3 edb分析的data dump
在这里插入图片描述
图5-4 ELF文件程序头
从data dump中可以看到,hello占用了0x400000~0x400100的空间保存各段信息, 与ELF文件中的程序头对应来看,他保存了PHDR、INTERP、LOAD、NOTE等段的信息。

5.5 链接的重定位过程分析

在这里插入图片描述
图5-5(a)objdump分析hello生成文件
在这里插入图片描述
图5-5(b) objdump分析hello生成文件
将其与图4-3作对比,可以发现在hello.o中call指令e8后面跟着的00 00 00 00都变成了有实际意义的数字,指向了要跳转函数的地址,并且在main函数的上方还有着很多使用到的函数的信息。链接就是锁定了这些符号的位置,并且对相关的信息进行了重定位。
重定位的方法是refptr = s + r.offset ; refaddr = ADDR(s) + r.offset ; *refptr = (unsigned)(ADDR(r.symbol) + r.attend – refaddr)

5.6 hello的执行流程

调用函数或地址
ld-2.27.so!start
ld-2.27.so!_dl_start
ld-2.27.so!_dl_start_user
ld-2.27.so!_dl_init
0x4005c0
libc-2.27.so!__libc_start_main
libc-2.27.so!__cxa_atexit
0x400680
0x400530
libc-2.27.so!_setjump
0x4005f2
0x400560
ld-2.27.so!_dl_fixup
ld-2.27.so!_dl_lookup_symbol_x
lbc-2.27.so!puts
libc-2.27.so!@plt
0x4005a0

5.7 Hello的动态链接分析

在这里插入图片描述
图5-6 _dl_init
在这里插入图片描述
图5-7 _dl_init执行前
在这里插入图片描述
图5-8 _dl_init执行后

5.8 本章小结

本章讲述了链接的概念与实现,查看了链接后的文件hello与可重定位目标文件hello.o之间的区别,对链接的过程有了相应的了解。

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程是一个执行中的程序的实例。
作用:进程给程序提供了两个关键的抽象。1.独立的逻辑控制流。2.私有的地址空间。

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

作用:Shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行。Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。不仅如此,Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。Shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果。
处理流程:shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序,然后shell在搜索路径里寻找这些应用程序。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。

6.3 Hello的fork进程创建过程

在shell中输入./hello,shell发现他不是内置命令,便把他当做可执行文件进行处理。shell通过fork创建一个创建一个子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程获得与父进程任何打开文件描述符相同的副本,这就意味着父进程调用fork时,子进程可以读写父进程任何打开的任何文件。父进程和新创建的子进程之间最大的区别在于他们拥有不同的PID。

6.4 Hello的execve过程

在刚刚fork的子进程中,调用execve函数加载hello。Execve函数在当前进程的上下文中加载并运行一个新的程序。execve加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以与fork调用一次返回两次不一样,execve调用一次并且从不返回。完成此过程之后,execve将PC改成hello的入口,进而执行hello。

6.5 Hello的进程执行

上下文信息:上下文就是内核重新启动一个被抢占的进程所需的状态。内核通过上下文切换的机制来将控制转移到新的进程。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
hello调度过程:如果hello在运行时没用收到挂起信号,那么他会一直在用户态下运行直到结束。否则,他收到挂起信号,sleep后陷入内核模式,计时器继续计时,内核利用上下文切换将控制转移到其他进程,sleep结束之后会发送一个中断信号,进程又陷入到内核模式,内核将控制还给hello,hello在用户态下继续进行自己的操作。

6.6 hello的异常与信号处理

hello执行过程中会出现中断、终止两种异常,会产生SIGSTP、SIGINT、SIGCONT、SIGKILL信号,通过内核调用异常处理程序进行处理。
在这里插入图片描述
图6-1 Ctrl-Z
在这里插入图片描述
图6-2 ps
在这里插入图片描述
图6-3 pstree
在这里插入图片描述
图6-4 fg
在这里插入图片描述
图6-5 jobs
在这里插入图片描述
图6-6 kill
在这里插入图片描述
图6-7 Ctrl-C

Ctrl-Z发送SIGSTP信号挂起进程,Ctrl-C发送SIGINT信号终止进程,ps列出当前的进程,jobs列出作业,pstree画出进程树,fg发送SIGCONT信号继续执行被中断的进程,kill发送SIGKILL信号杀死某进程。

6.7本章小结

本章介绍了hello的如何被shell通过fork,execve运行,也阐述了shell的概念与作用,介绍了hello是如何执行的,着重讲了hello执行过程中的用户态与核心态之间的转换是通过上下文切换。在hello运行中使用了一些指令以发送信号改变hello运行状态。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址是指在计算机体系结构中是指应用程序角度看到的内存单元(memory cell)、存储单元(storage element)、网络主机(network host)的地址。在hello中就是程序中能够看到的偏移地址。
线性地址:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。在hello中就是虚拟地址。
虚拟地址:虚拟地址是Windows程序时运行在386保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。在hello中就是链接之后得到的地址。
物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。在hello中就是他在主存上存放的地址。

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

段式管理(segmentation),是指把一个程序分成若干个段(segment)进行存储,每个段都是一个逻辑实体(logical entity),程序员需要知道并使用它。它的产生是与程序的模块化直接有关的。段式管理是通过段表进行的,它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。
在这里插入图片描述
图7-1 逻辑地址到线性地址的转换

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

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

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

以Core i7为例,VA被划分为虚拟页号VPN和虚拟页偏移量VPO两部分,36位的虚拟页号VPN被划分成4段(每段9位),每一段都指向一个页表(前三级)或PTE(最后一级)。根据每一级的VPN和从上一级得到的页表目录从而得到下一级的页表目录,直到最后一级通过VPN4得到相应的PPN,再用PPN与PPO(与VPO相同)组合生成PA。
在这里插入图片描述
图7-3 TLB与四级页表支持下的VA到PA的变换

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

得到的PA被翻译成CT+CI+CO三部分,根据这个地址到L1中寻找相应的块,若找不到则到L2中找,若还找不到则依次在L3、主存当中找直到找到,在进行相应的页替换。
在这里插入图片描述
图7-4 三级Cache支持下的物理内存访问

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

exceve函数加载和执行程序Hello,需要以下几个步骤:

  1. 删除已存在的用户区域。
  2. 映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。
  3. 映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
  4. 设置程序计数器(PC)。

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

缺页故障:缺页中断就是要访问的页不在主存,需要操作系统将其调入主存后再进行访问。在这个时候,被内存映射的文件实际上成了一个分页交换文件。
缺页中断处理:

  1. 硬件陷入内核,在内核堆栈中保存程序计数器。大多数机器将当前指令的各种状态信息保存在特殊的CPU寄存器中。
  2. 启动一个汇编代码例程保存通用寄存器和其他易失的信息,以免被操作系统破坏。这个例程将操作系统作为一个函数来调用。
  3. 当操作系统发现一个缺页中断时,尝试发现需要哪个虚拟页面。通常一个硬件寄存器包含了这一信息,如果没有的话,操作系统必须检索程序计数器,取出这条指令,用软件分析这条指令,看看它在缺页中断时正在做什么。
  4. 一旦知道了发生缺页中断的虚拟地址,操作系统检查这个地址是否有效,并检查存取与保护是否一致。如果不一致,向进程发出一个信号或杀掉该进程。如果地址有效且没有保护错误发生,系统则检查是否有空闲页框。如果没有空闲页框,执行页面置换算法寻找一个页面来淘汰。
  5. 如果选择的页框“脏”了,安排该页写回磁盘,并发生一次上下文切换,挂起产生缺页中断的进程,让其他进程运行直至磁盘传输结束。无论如何,该页框被标记为忙,以免因为其他原因而被其他进程占用。
  6. 一旦页框“干净”后(无论是立刻还是在写回磁盘后),操作系统查找所需页面在磁盘上的地址,通过磁盘操作将其装入。该页面被装入后,产生缺页中断的进程仍然被挂起,并且如果有其他可运行的用户进程,则选择另一个用户进程运行。
  7. 当磁盘中断发生时,表明该页已经被装入,页表已经更新可以反映它的位置,页框也被标记为正常状态。
  8. 恢复发生缺页中断指令以前的状态,程序计数器重新指向这条指令。
  9. 调度引发缺页中断的进程,操作系统返回调用它的汇编语言例程。
  10. 该例程恢复寄存器和其他状态信息

7.9动态存储分配管理

动态内存分配器维护者一个进程的虚拟内存区域,称为堆.
分配器将堆视为一组不同大小的 块(blocks)的集合来维护,每个块要么是已分配的,要么是空闲的。
分配器的类型:
显式分配器: 要求应用显式地释放任何已分配的快
例如,C语言中的 malloc 和 free
隐式分配器: 应用检测到已分配块不再被程序所使用,就释放这个块
比如Java,ML和Lisp等高级语言中的垃圾收集 (garbage collection)
隐式空闲链表 (Implicit list) 通过头部中的大小字段—隐含地连接所有块,如果块是对齐的,那么一些低阶地址位总是0,不要存储这些0位,而是使用它作为一个已分配/未分配的标志,读大小字段时,必须将其屏蔽掉。
显式空闲链表在空闲块中使用指针连接空闲块。保留空闲块链表, 而不是所有块,“下一个” 空闲块可以在任何地方,因此我们需要存储前/后指针,而不仅仅是大小,还需要合并边界标记,幸运的是,我们只跟踪空闲块,所以我们可以使用有效区域。

7.10本章小结

本章讲述了hello的存储器地址空间和虚拟地址、线性地址、物理地址、逻辑地址之间的转换,还讲述了Intel是如何利用TLB与页表还有Cache进行内存访问的,也介绍了fork函数与execve函数在建立内存映射时的区别,最后介绍了动态内存分配器的种类与实现方式。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件
设备管理:unix io接口
所有的I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许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函数:

int open(char *filename, int flags, mode_t mode);
int close(int fd);
ssize_t read(int fd, void* buf, size_t n);
ssize_t write(int fd, const void* buf, size_t n);

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;
}

可以看到printf是通过vsprintf与write实现的。write不必深究,看看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是将参数的内容格式化之后存入buf,返回的是格式化之后的数组的长度。
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

int getchar(void)

{

    static char buf[BUFSIZ];

    static char* bb=buf;

    static int n=0;

    if(n==0)

    {

        n=read(0,buf,BUFSIZ);

        bb=buf;

    }

    return(--n>=0)?(unsigned char)*bb++:EOF;

}

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

8.5本章小结

本章只是简单的介绍了Linux的I/O方式和Unix I/O的接口,并且大概的阐述了printf与getchar的实现,不过这些解释都还是很肤浅的。

结论

hello所经历的过程:

  1. 程序员编写hello.c
  2. 预处理:hello.c–>hello.i
  3. 编译:hello.i–>hello.s
  4. 汇编:hello.s–>hello.o
  5. 链接:hello.o–>hello
  6. 在shell中运行./hello
  7. shell调用fork,execve加载hello,并建立虚拟内存的映射
  8. CPU分hello时间片,hello的指令按照时间片和控制逻辑在CPU中执行
  9. 通过TLB、页表、Cache、Pagefile访存
  10. 使用I/O接口进行输出
  11. 结束后祖先进程回收hello

计算机系统本身就是一个网络,硬件软件相互配合最终才实现了程序的完美运行。

附件

中间结果作用
hello.i预处理得到的文件
hello.s汇编语言文件
hello.o可重定位目标文件
hello可执行目标文件
elf1.txthello.o的elf文件
hellooobj.txthello.o的反汇编文件
elf2.txthello的elf文件
helloobj.txthello的反汇编文件

参考文献

为完成本次大作业你翻阅的书籍与网站等
[1] 深入理解计算机系统 机械工业出版社
[2] 百度百科
[3] printf函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值