计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 120L022413
班 级 2003008
学 生 左曾元
指 导 教 师 吴锐
计算机科学与技术学院
2022年5月
摘 要
本文分析hello程序从C文件转变为可执行文件的过程,介绍了hello程序在Linux系统下的生命周期。对预处理、编译、汇编、链接的过程进行了分析,并讨论了hello的进程管理、存储管理以及IO管理,介绍了汇编指令、机器代码、重定位、动态链接、异常控制流、虚拟内存以及IO函数的相关内容,对hello程序的一生进行了的描述,简单经过了整个计算机系统。
关键词:程序的一生,预处理,编译,汇编,链接,IO,进程管理,存储管理
目 录
第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 -
根据hello的自白。hello的P2P过程是从一个程序员编写的源文件hello.c开始。hello.c经过预处理器cpp的预处理生成修改了的源程序hello.i,编译器ccl的编译生成汇编程序hello.s、汇编器as的汇编生成可重定位目标程序hello.o,最后通过链接器ld的链接生成可执行目标程序hello。当在终端中输入./hello后,shell会调用fork函数创建一个新的进程,然后调用execve将其加载到内存中,此时hello便从一个程序变为了进程。hello的020过程是,shell首先fork一个子进程,然后通过execve加载并执行hello,映射虚拟内存,进入程序入口后将程序载入物理内存,进入主函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。
1.2.1 硬件环境
X64 CPU; 1.50GHz;16.0GB RAM;256GHD Disk
1.2.2 软件环境
Ubuntu 20.4.4;VirtualBox;windows11 64位
1.2.3 开发工具
Visual Studio 2019 64 位;CodeBlocks 64位;GDB
1.3.1 hello.i:
hello.c预编译的结果,用于研究预编译的作用以及进行编译器的下一步编译操作;
1.3.2 hello.s:hello.i编译后的结果,用于研究汇编语言以及编译器的汇编操作,可以与hello.c对应,分析底层的实现;
1.3.3 hello.o:hello.s汇编后的结果,可重定位目标程序,没有经过链接,用于链接器或编译器链接生成最终可执行程序;
1.3.4 hello.out:hello.o链接后生成的可执行目标文件,可以通过反汇编分析程序运行过程。
1.3.5 hello:由gcc -m64 -no-pie -fno-PIC hello.c -o命令生成。
本章简要介绍了hello程序从代码到编译生成,到执行,再到终止的过程与操作系统发生的事件,分析了其P2P和020的过程,列出了本次任务的硬件、软件环境和调试工具,并且列举了任务过程中出现的中间产物及其作用。
预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
预处理实现的工作:头文件展开、去注释、条件编译、宏替换。
头文件展开是通过编译器找到指定的头文件,并将头文件对应的内容拷贝至源文件,宏替换是在hello.i文件中宏替换成了它所代表的内容,条件编译是满足条件的才会在hello.i文件中编译出来。
预处理命令:gcc -E hello.c -o hello.i
经过预处理后,hello.c文件生成了hello.i文件,打开hello.i文件后可见文件扩展了很多行。原文件中包含的头文件的内容被插入到了该文件中。
本章了解了预处理的概念和作用,以及虚拟机下预处理的命令,并对预处理的结果进行解析。预处理是计算机对程序进行操作的第一个步骤,对头文件、宏定义和注释进行操作,生成hello.i文件。
编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。这个过程称为编译,同时也是编译的作用。是把源程序(高级语言)翻译成目标程序。除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。
编译命令:gcc -S hello.i -o hello.s
字符串:
存储在只读数据段(.rodata)。
局部变量:
Hello.c中局部变量i存储在堆栈端,i是int类型,开辟了四个字节。
全局变量:
可以看出main是一个全局变量。
数组:
表示对argv[]数据中的数据进行获取。
通过cmpl和je比较是否相等。
控制转移:
跳转到.L3
类型转换:
通过atoi将字符串转换为整数。
函数操作:
通过call调用printf()和sleep()函数。
赋值:通过movl和movq进行赋值操作。
本章主要探究了编译阶段hello.i到hello.s的转变,更深入理解了编译器对数据的一些操作。
汇编是指经过编译后,汇编器as将汇编语言翻译成机器语言,得到可重定位目标文件的过程。实现将汇编代码转换为机器指令,使之在链接后能够被计算机直接执行。
汇编的命令:as hello.s -o hello.o
命令:readelf -a hello.o > ./elf.txt
ELF头以一个16字节的序列开始,这个序列描述了生成该文件系统下的字的大小以及一些其他信息。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
节头表描述了.o文件中每一个节出现的位置,大小,目标文件中的每一个节都有一个固定大小的条目。
重定位节中包含了在代码中使用的一些外部变量等信息,在链接的时候需要根据重定位节的信息对这些变量符号进行修改。链接的时候链接器会根据重定位节的信息对外部变量符号决定选择何种方法计算正确的地址,通过偏移量等信息计算出正确的地址。
.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。例如本程序中的getchar、puts、exit等函数名都需要在这一部分体现
命令:objdump -d -r hello.o > disas_hello.s
将其与hello.s进行比较可知,其余汇编语言的指令并没有什么不同,反汇编代码中包含机器代码。机器语言是用二进制表示的语言,也是机器能够真正识别的语言,机器指令由操作码和操作数构成。每一条汇编语言中的指令都可以对应于唯一的一个二进制数据表示,而汇编语言的操作数本身也可以表示为二进制数据,故每一条汇编语言的指令,即操作码+操作数都可以与机器指令建立一一对应的映射关系。从而能够通过这种映射关系将机器指令反汇编为我们更熟悉的、更接近CPU的动作和运行原理的汇编语言。
反汇编的分支转移指令中的操作数用的并不是段的名称,如.L2、.L3等,而是用的确定的地址,如2d
在汇编语言中,函数调用的指令中使用的是函数名称,而在反汇编中,call指令后面的地址为call指令之后的一条指令的地址。因为 hello.c中调用的函数都是共享库中的函数,故需要通过等待调用动态的链接把重定位的函数目标地址链接到共享库程序当中,最终需要通过动态链接器才能确定函数的运行时地址。在汇编成为机器语言时,对于这些不确定地址的函数调用,直接将其call指令后的相对地址设置为0,则目标地址正是下一条指令的地址,等待链接的进一步确定。
本章主要介绍了汇编的概念和作用,分析了可重定位目标elf,同时通过反汇编解析了将hello.s汇编后生成的hello.o的结果,分析了汇编语言与机器语言之间的关系。
链接是将各种不同文件的代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。把预编译好了的若干目标文件合并成为一个可执行目标文件。使得分离编译称为可能,不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。当改变这些模块中的一个时,只需简单重新编译它并重新链接即可,不必重新编译其他文件。
使用ld的链接命令:
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
命令:readelf -a hello > hello1.elf
Elf的头,以及起始地址,大小等信息
使用edb加载hello,查看本进程的虚拟地址空间各段信息:
PHDR:保存了二进制的程序头表。
INTERP:指定在程序已经从可执行文件映射到具体的内存区域之后,必须进程调用的解释器(如动态链接器)。
LOAD:表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据、程序的目标代码等
DYNAMIC:保存了由动态链接器使用的信息。
NOTE:保存辅助信息
GNU_STACK:权限标志,标志栈是否是可执行
GNU_RELRO:这个节中指定了在重定位程序结束之后那些映射到内存的区域是需要重新设置为只读类型的。
用edb的data dump即可查询各个程序头部的信息。
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
一旦链接器完成了符号解析这一步,就把代码中的每个符号引用和正好一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。现在就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时地址。重定位由两步组成:
1.重定位节和符号定义:链接器将所有类型相同的节合并为同一类型的新的聚合节。然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
2.重定位节中的符号引用:这一步中,链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。当汇编器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
401000 <_init>
401020 <.plt>
401030 puts@plt
401040 printf@plt
401050 getchar@plt
401060 atoi@plt
401070 exit@plt
401080 sleep@plt
401090 <_start>
4010c0 <_dl_relocate_static_pie>
401150 <__libc_csu_init>
4011b0 <__libc_csu_fini>
4011b4 <_fini>
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
当程序调用一个由共享库定义的函数时,由于编译器无法预测这时候函数的地址是什么,因此这时,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时。通过GOT和过程链接表PLT的协作来解析函数的地址。在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。那么,通过观察edb,便可发现dl_init后.got.plt节发生的变化。
前:
后:
在链接过程中,各种代码和数据片段收集并组合为一个单一文件。利用链接器,分离编译称为可能,我们不用将应用程序组织为巨大的源文件,只是把它们分解为更小的管理模块,并在应用时将它们链接就可以完成一个完整的任务。
经过链接,已经得到了一个可执行文件。
进程是执行中程序的抽象。在现代系统上运行一个程序时,我们会得到一个假象,好像我们的程序是系统中唯一运行的程序一样。我们的程序好像独占处理器和内存。处理器好像无间断地一条接一条执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象是通过进程的概念提供的。进程提供给应用程序的关键抽象:1)一个独立的逻辑控制流,提供一个程序独占处理器的假象;2)一个私有的地址空间,提供一个程序独占地使用内存系统的假象。
shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤解析命令行,并根据解析结果运行程序。
以下为处理流程:
(1)终端进程读取用户由键盘输入的命令行。
(2)将输入的命令行字符串切分,获取命令行参数,并构造传递给execve的argv向量。
(3)检查第一个命令行参数是否是一个内置的shell命令
(4)如果是内置命令则立即执行;如果不是内部命令,调用fork创建子进程。
(5)在子进程中,用获取的命令行参数调用execve执行指定程序。
(6)如果命令末尾没有&号,则说明进程要前台运行,shell使用waitpid等待作业终止后返回。
(7)如果命令末尾有&号,则说明进程要后台运行,shell直接返回。
父进程通过调用fork函数创建一个新的运行的子进程。调用fork函数后,新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本,包括代码、数据段、堆、共享库以及用户栈,子进程获得与父进程任何打开文件描述符相同的副本,这意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。fork被调用一次,却返回两次,子进程返回0,父进程返回子进程的PID。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
exceve函数在当前进程的上下文中加载并运行一个新程序。exceve函数加载并运行可执行目标文件,并带参数列表和环境变量列表。只有当出现错误时,exceve才会返回到调用程序。所以,与fork一次调用返回两次不同,在exceve调用一次并从不返回。当加载可执行目标文件后,exceve调用启动代码,启动代码设置栈,将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序,由此将控制传递给新程序的主函数。
进程调度:即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一的对应于包含在运行时动态链接到程序的共享对象中的指令。这个PC的序列叫做逻辑控制流,或者简称逻辑流。进程是轮流适用处理器的,每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
进程执行到某些时刻,内核可决定抢占该进程,并重新开启一个先前被抢占了的进程,这种决策称为调度。内核调度一个新的进程运行后,通过上下文切换机制来转移控制到新的进程:1)保存当前进程上下文;2)恢复某个先前被抢占的进程被保存的上下文3)将控制转移给这个新恢复的进程。当内核代表用户执行系统调用时,可能会发生上下文切换,这时就存在着用户态与核心态的转换。
下面是正常执行:
当输入不正确的时候,提示正确输入格式:
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏。
输入ctrlc会终止程序:
Crtlz挂起前台作业,ps查看目前的进程。
一顿乱按会被getchar()读入,寻找一个输入的回车作为字符串结尾,然后程序正常终止结束并被回收:
按下kill:
本章主要介绍了进程的概念与作用,简述了shell的作用与处理流程,并详细地分析了hello的fork进程创建过程、execve过程以及hello进程的执行过程,并在最后举例总结了hello的异常与信号处理。
逻辑地址:格式为“段地址:偏移地址”,是CPU生成的地址,在内部和编程使用,并不唯一。
物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址。
虚拟地址:保护模式下程序访问存储器所用的逻辑地址。
线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址。
8086共设计了20位宽的地址总线,通过将段寄存器左移4位加上偏移地址得到20位地址,即逻辑地址。将内存分为不同的段,每个段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。VM系统将虚拟内存分割,称为虚拟页,类似地,物理内存也被分割成物理页。利用页表来管理虚拟页,页表就是一个页表条目(PTE)的数组,每个PTE由一个有效位和一个n位地址字段组成,有效位表明了该虚拟页当前是否被缓存在DRAM中,如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置。如果发生缺页,则从磁盘读取。
如果按照上述模式,每次CPU产生一个虚拟地址并且发送给地址管理单元,MMU就必须查找一个PTE行来用将虚拟地址翻译成物理地址。为了消除这种操作带来的大量时间开销,MMU中被设计了一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB)也叫快表。例如当每次cpu发现需要重新翻译一个虚拟地址时,它就必须发送一个得到虚拟地址mmu,发送一个vpo位得到一个l1高速缓存.例如当我们使用mmu向一个tlb的组请求一个页表中的条目时,l1高速缓存通过一个vpo位在页表中查找一个相应的数据标记组,并在页表中读出这个组里的个数据标记和相应的数据关键字.当mmu从一个tlb的组得到一个ppn时,代表缓存的工作在这个组的请求之前已经完全准备好,这个组的ppn与就已经可以与这些数据标记文件中的一个虚拟地址进行很好的匹配了.
corei7采用四级页表层次结构,每个四级页表进程都有他自己私有的页表层次结构,这种设计方法从两个基本方面就是减少了对内存的需求,如果一级页表的pte全部为空,那么二级页表就不会继续存在,从而为进程节省了大量的内存,而且也只有一级页表才会有需要总是在一个内存中。四级页表的层次结构操作流程如下:36位虚拟地址被寄存器划分出来组成四个9位的片,每个片被寄存器用作到一个页表的偏移量。cr3寄存器内储存了一个l1页表的一个物理起始基地址,指向第一级页表的一个起始和最终位置,这个地址是页表上下文的一部分信息。1提供了到一个l1pet的偏移量,这个pte寄存器包含一个l2页表的起始基地址.2提供了到一个l2pte的偏移量,一共四级,逐级以此层次类推。
通过内存地址的组索引获得值,如果对应的值是data则像L1 d-cache对应组中查找,如果是指令,则向L1 i-cache对应组中查找。将L1对应组中的每一行的标记位进行对比,如果相同并且有效位为1则命中,获得偏移量,取出相应字节,否则不命中,向下一级cache寻找,直到向内存中寻找.
shell通过一个调用用fork的函数可以让进程内核自动创建一个新的进程,这个新的进程拥有各自新的数据结构,并且被内核分配了一个唯一的pid。它有着自己独立的虚拟内存空间,并且还拥有自己独立的逻辑控制流,它同样可以拥有当前已经可以打开的各类文件信息和页表的原始数据和样本,为了有效保护进程的私有数据和信息,同时为了节省对内存的消耗,进程的每个数据区域都被内核标记起来作为写时复制。
1.删除已存在的用户区域. 删除当前继承虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域. 为新程序的代码,数据,bss和站区域创建新的区域结构.所有这些新的区域都是私有的,写时复制的.代码和数据区域被映射为hello.out文件中的.text和.data取. bss区域时请求二进制零的。
3.映射共享区域. 如果hello.out程序与共享对象(或目标)链接,比如标准c库 libc.so, 那么这些对象都是动态链接这个程序的, 然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC). execve做的最后一件事就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
如果程序执行过程中遇到了缺页故障,则内核调用缺页处理程序。处理程序会进行如下步骤:检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。然后检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。在两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。
基本方法与策略:通过维护虚拟内存(堆),一种是隐式空闲链表,一种是显式空闲链表。显式空闲链表法是malloc(size_t size)每次声明内存空间都保证至少分配size_t大小的内存,双字对齐,每次必须从空闲块中分配空间,在申请空间时将空闲的空间碎片合并,以尽量减少浪费。
本章讨论了存储空间的管理,首先解释了四种地址的概念,逻辑地址、线性地址、虚拟地址、物理地址,接着描述了逻辑地址到线性地址再到物理地址的转换过程。再用TLB和四级页表的实例描述了va到pa的转变过程,说明了如何使用物理内存访问cache。在将进程使用到的fork和execve的内存映射情况加以描述。
Linux中的I/O设备被表示为文件,输入输出用文件的读和写来执行。
设备的模型化:文件
设备管理:unix io接口
Unix IO接口:
打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件
改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k。
关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中
Unix IO函数:
(1).打开文件:int open(char *filename, int flags, mode_t mode);
Open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程当中没有打开的最小描述符。Flags参数指明了进程打算如何访问这个文件,同时也可以是一个或者更多为掩码的或,为写提供给一些额外的指示。Mode参数指定了新文件的访问权限位。
(2).关闭文件:int close(int fd);
调用close函数,通知内核结束访问一个文件,关闭打开的一个文件。成功返回0,出错返回-1。
(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);
调用从内存位置buf复制至多n个字节到描述符fd的当前文件位置。返回值-1表示出错,否则,返回值表示内存向文件fd输出的字节的数量。
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中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址。syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
Linux提供了一种简单使用的抽象——将系统的IO设备抽象成文件,系统的输入和输出被抽象成文件的写和读操作。在此基础上,Linux对系统IO的操作可以以打开文件、改变文件位置、读写文件、关闭文件的操作进行。同时分析了printf和getchar的实现。
用计算机系统的语言,逐条总结hello所经历的过程。
对计算机系统的设计与实现的深切感悟,我的创新理念
hello的一生每一个c语言程序执行前的流程,总共分为9步:
在计算机系统的设计与实现过程中所必须要满足的就是准确,程序的执行必须能输出准确的结果,在这一基础上进行一定的优化能够让程序执行的更快,包括cache,流水线,超标量等设计都是基于这些的。在完成大作业的过程中相当于回顾了一遍这学期的学习内容,对于计算机系统设计与实现也有了更深切的感悟。
列出所有的中间产物的文件名,并予以说明起作用。
hello.c源代码
hello.i预处理得到的文件
hello.o汇编生成的可重定位目标文件
hello.s编译后得到的汇编语言
disas_hello.s反汇编得到的文件
elf.txt hello.o的elf格式
hello链接后生成的可执行文件
hello.elf反汇编的汇编代码
hello_objdump.s hello反汇编生成的文件
[1]Randal E.Bryant David R.O'Hallaron深入理解计算机系统2021.10第一版[2]http://docs.huihoo.com/c/linux-c-programming/C汇编Linux手册
[3]http://csapp.cs.cmu.edu/3e/labs.html CMU的实验参考
[4] http://cn.ubuntu.com/ http://forum.ubuntu.org.cn/网站与论坛
[5]计算机系统大作业——程序人生-Hello’s P2P_sora0v0的博客-CSDN博客
[6]哈工大 计算机系统 大作业 程序人生-Hello’s P2P - zsh1234 - 博客园
[7]哈工大计算机系统大作业--程序人生_RaccoonCommando的博客-CSDN博客
[8]https://www.cnblogs.com/pianist/p/3315801.html