本文旨在分析hello.c在linux系统中的整个生命周期,利用edb等反汇编工具,分析hello程序从hello.c开始,经历预处理,编译,汇编,链接等过程最终生成可执行文件的全过程。同时其中涉及了计算机系统的进程管理,储存管理,I/O管理。对计算机系统做到更加深刻的理解。
关键词:Hello.c;预处理;编译;汇编;链接;进程管理;储存管理;IO
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式....................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程........................................................................ - 10 -
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 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:即 From Program to Process.Hello.c是写好的程序的文本文件,编写Hello.c的过程即program.之后,hello.c经过cpp预处理生成hello.i。经过编译器编译后生成hello.s。之后由汇编器重定位为hello.o。最后由链接器与库函数链接的到可执行文件hello。之后在shell中启动hello程序时,shell为hello程序fork一个子进程,hello由程序变为进程。即From Program to Process。
020:shell通过execve加载执行hello程序。操作系统为其分配虚拟空间,并且映射到物理内存。由CPU处理hello程序的指令,实现功能。在程序结束后,shell回收程序,操作系统释放为其分配的内存空间。Hello程序就此结束。即020.:From Zero-0 to Zero-0。
1.2 环境与工具
软件环境:windows 10 64 位, VMware Workstation Pro,Ubuntu 20.04 LTS 64 位
硬件环境:Intel®Core™i7-10750H CPU@2.60GHz;16G RAM;512GB Disk
开发工具:gcc,gdb,vscode,edb
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
Hello.i : 预处理后的hello.c 分析预处理器的行为
hello.s hello.i的汇编结果,分析汇编器行为
Hello.o: 可重定位目标程序。
1.4 本章小结
本章简单介绍了hello的一生,即hello的P2P和020的过程,并且列出了本次实验所需环境以及中间结果等。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:在程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。以C/C++预处理为例:ISO C和ISO C++都规定程序由源代码被翻译分为若干有序的阶段(phase) ,通常前几个阶段由预处理器实现。预处理中会展开以#起始的行,试图解释为预处理指令(preprocessing directive) ,其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令) 。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
图2.1.1-hello.c中的头文件
作用:(1)文件包含:作用是将另一个源文件加入到当前源文件中该点处。
(2)条件编译:预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
(3)宏展开:预处理器将源程序文件中出现的对宏的引用展开成相应的宏定义,即#define的功能,由预处理器来完成。
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i
图2.2-1 预处理命令
2.3 Hello的预处理结果解析
图 2.3-1 预处理文件
在经过预处理后,hello.c文件转化为了hello.i文件。可以看到文件增加了更多的内容,所增加的内容为宏展开后的头文件内容。
在头文件展开过程中,若遇到了#define,预处理器会对此进一步地展开。
2.4 本章小结
本章介绍了预处理的一些相关概念及作用,并通过对Hello.c的文件的预处理对预处理进行直观了解。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:此处编译的概念即把hello.i编译为hello.s.即将源代码翻译为汇编语言。
作用:编译器在此处会对代码进行优化。
进行词法分析和语法分析,可以找出语法错误并给出提示信息。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图 3.2-1 编译命令
图3.2-2 hello.s文件
3.3 Hello的编译结果解析
3.3.1 字符串的处理
图3.3-1 字符串处理
.string : 这是对字符串的处理,两个要打印的字符串分别储存在LC0和LC1中。
3.3.2 参数的储存
(1)argc:
图3.3-2 .c文件中的argc
图3.3-3 argc
可以看到,源文件中argc与4进行比较,而在汇编文件中,%edi中的数据传给了-20(%rbp)之后与4进行了比较,所以argc储存在%edi中。
(2)i:
图 3.3-4 .c文件中的i
(a) (b)
图3.3-5 i
可以看到,源文件中有i<8的条件。而L2中%rbp被赋值为0后,跳转到L3进行了与7的比较,所以i储存在%rbp中。
(3)argv(数组):
图3.3-6 argv
L4中的这部分汇编语言里,调用了-32(%rbp)可以看出是在调用argv数组的四个参数。可以看出是由%rsi传来的。
3.3.3 赋值
图3.3-7 赋值
利用movl,将0赋值给i. 其中l是32位的意思。因为int是四个字节
3.3.4 算数操作
图3.3-8 算数
此处为对i++的实现,使用了addl,l是32位。原因同上。
3.3.5 关系操作
(1)
图3.3-9 关系(1)
比较argc是不是4,是的话则跳转到L2.
(2)
图3.3-10 关系(2)
比较i是否小于8,小于8则跳转到L4.
3.3.6 控制转移
(1)if(agrc!=4)
图3.3-11 if控制转移
用cmpl和je来实现,在关系部分已经解释过。
(2)for(i=0;i<8;i++)
图3.3-12 for循环
用cmpl将i与7作比较,小于等于则跳转,同时,L4的末尾有add $1的操作,实现for循环。
3.3.7函数操作
在此hello.s中,函数调用另一个函数都是先进行参数传递,然后利用call转到相应的函数入口进行执行的。
printf():%rdi传递参数:
图3.3-13 printf函数
exit():%edi传递参数;
图3.3-14 exit函数
atoi():%rdi传递参数:
图3.3-15 atoi函数
sleep:%edi传递参数:
图3.3-16 sleep函数
3.4 本章小结
本章主要介绍了编译的概念及作用,并且对汇编代码中的各种数据类型,函数,控制流程进行了分析和展示,对编译过程有更详细的理解。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器将汇编语言翻译为机器语言,同时将指令打包为可重定位目标程序的格式,保存至hello.o中。
作用:将汇编语言翻译为机器语言(二进制代码),使机器能够直接识别。
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o
图4.2-1 汇编命令
4.3 可重定位目标elf格式
命令:readelf -a hello.o
图4.3-1 可重定位目标elf格式
4.3.1 ELF头
如图所示:
图4.3-2 ELF头
4.3.2 节头表
节头部表描述了不同节的名称,大小和位置。其中目标文件中的每个节都有一个固定的大小条目。
如图所示:
图4.3-3 节头表
4.3.3 重定位条目
图4.3-4 重定位条目
包含text节中需要重定位的信息,当链接器将这个文件和其它文件链接时,这个位置的信息需要更新。八条重定位函数,分别对于.L0,puts,exit,.L1,printf,atoi,sleep,getchar进行重定位。
4.3.4 符号表
图4.3-5 符号表
符号表中存放了程序中定义和引用的函数和全局变量。在hello.c中没有定义全局变量,所以符号表中只有函数而无全局变量。
4.4 Hello.o的结果解析
图4.4-1 hello.o的反汇编
不同:1反汇编的语言add ,mov,push后无其他字符如w、l等。
2反汇编中的call直接call一个数字,而没有call函数名。
3对全局变量的访问方式:反汇编语言通过pc相对寻址,通过rip+x的值进行访问,未重定位,故用0占位。在汇编语言中,通过.LC0+rip进行访问。
4 反汇编中,函数没有被分开,所有代码使一整段。
4.5 本章小结
本章介绍了编译过程,该过程是将汇编语言翻译为机器语言的过程,从而使计算机识别并执行相关指令,得到了.o文件。并且通过readelf查看了其基本信息。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接指的是计算机程序中各个模块之间传递参数和控制命令,将程序编码和数据收集整理为一个单一的文件,生成可执行目标文件的过程。
作用:链接方便了程序的修改和编译,可以让我们独立的编译某一小部分的代码,并重新链接应用。
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.2-1 链接
5.3 可执行目标文件hello的格式
指令:readelf -a hello > hello1.elf
图5.3-1 生成hello11.elf
图5.3-2 节的个数
在elf头中,可以看出hello是一个可执行文件 ,有27个节。
5.3.1节头:
图5.3-3 节头
节头对hello中的所有的节信息进行了声明,包括大小,偏移量等。
5.3.2.重定位节.rela.text
图5.3-4 重定位节
5.3.3.符号表.symtab
图5.3-5 符号表
5.4 hello的虚拟地址空间
图5.4-1 程序头部分
可以看到LOAD可加载的程序段地址为0x400000.
图5.4-2 data dump截图
程序从0x40000开始,到0x400fff结束。可以根据5.3中的头部表,在edb中找到各个节的信息。如.plt在0x401020.
图5.4-3 .plt位置
5.5 链接的重定位过程分析
命令:objdump -d -r hello > hello22.txt
图5.5-1 .hello反汇编
(1)与hello.o的反汇编文件进行对比,hello22.txt中多了很多节。
(2)在hello的反汇编当中,相对于hello.o的反汇编,重定位地址已经填入了对应的正确信息。
(3)hello中增加了hello.o中没有的函数,比如printf,sleep等。这是由于链接时完成了符号解析和重定位。
图5.5.2 Hello.22 部分截图
5.6 hello的执行流程
Hello!_init
Hello!puts@plt
Hello!printf@plt
Hello!getchar@plt
Hello!atoi@plt
Hello!exit@plt
Hello!sleep@plt
Hello!.plt+0x70
Hello!.plt.got+0x10
Hello_start
Hello!main
Hello!__libc_csu_init
Hello!__libc_csu_fini
exit
5.7 Hello的动态链接分析
图5.7-1 got.plt
图 5.7-2 dl_init之前
图 5.7-3 dl_init之后
可以看到运行dl_init之后,。Got.plt的值从0变为非0.相应的,调用的各函数也都从0有了地址。
5.8 本章小结
本章主要介绍了链接的过程,可重定位文件经过静态链接以及动态链接后成为了可执行程序。链接后的代码体积变小。通过对各种文件即反汇编文件的分析,对链接的文件变化有了更深入的认识。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
作用: 进程使得处理器好像是无间断的执行程序中的指令,使处理器认为程序中的代码和数据是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
shell是用户和Linux(或者更准确的说,是你和Linux内核)之间的接口程序。用户在提示符下输入的每个命令都由shell先解释然后传给Linux内核。
shell 是一个命令语言解释器(command-language interpreter)。拥有自己内建的 shell 命令集。此外,shell也能被系统中其他有效的Linux 实用程序和应用程序(utilities and application programs)所调用。
处理流程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(4)如果不是内部命令,调用fork( )创建新进程/子进程
(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait等待作业终止后返回。
(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。
父进程和子进程最大的不同时他们的PID是不同的,父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。
对于hello来说,我们输入了命令参数后,因为命令不是系统内部的命令,所以需要fork创建一个子进程。
6.4 Hello的execve过程
xecve 函数加载并运行可执行目标文件filename, 且带参数列表argv 和环境变量列表envp .只有当出现错误时,例如找不到filename, execve 才会返回到调用程序.所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回.
execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段.新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容.最后加载器设置PC指向_start地址,_start最终调用main函数.除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制.直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存.
图6.4-1 execve
6.5 Hello的进程执行
图6.5-1 逻辑流
系统中每个程序都运行在某个进程的上下文中。上下文是程序正确运行所需要的状态,由内核进行维持。
一个运行多个进程的系统,进程逻辑流的执行可能是交错的。每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。一个逻辑流在时间上与另一个重叠,成为并发流。一个进程执行它的控制流的一部分时间叫做时间片。
控制寄存器利用模式位描述了当前进程享有的特权:当设置了模式位时,进程运行在内核模式中,可以执行任何命令,访问任何内存;当没有设置模式位时,进程为用户模式,不允许执行特权指令,不允许直接引用内核区的代码、数据。
在进程执行时,内核可以抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策称为调度。当进程调度一个新的进程运行后,会使用上下文切换来将控制转移到新的进程。上下文切换会:1.保存当前进程的上下文。2.恢复某个先前被抢占进程的被保存的上下文。3.将控制传递给新进程。系统调用、中断可能引起上下文切换。
6.6 hello的异常与信号处理
1 异常和信号异常种类
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令或终止
终止 不可恢复的错误 同步 不会返回
2 运行结果
(1)正常
图6.6-1 正常
(2)按下ctrl-z
图6.6-2 ctrl-z
Ctrl-z默认结果是挂起前台程序的作业,hello进程并没有被回收,而是依然在后台运行。
图6.6-3 ps1
通过ps可以看到hello仍然在后台。
(3)ctrl-c
图6.6-4 ctrl-c
ctrl-c会将前台作业终止
图6.6-5 ps2
可以看到此时并无hello程序
(4)乱按
图6.6-6 乱按
无关输入被缓存到stdin,并随着printf指令被输出到结果。
6.7本章小结
本章介绍了进程的概念和作用并且分析了fork()创建新进程,调用execve函数执行hello,hello进程执行过程,以及hello在运行时遇到的异常与信号处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是指在计算机体系结构中是指应用程序角度看到的内存单元(memory cell)、存储单元(storage element)、网络主机(network host)的地址。 逻辑地址往往不同于物理地址(physical address),通过地址翻译器(address translator)或映射函数可以把逻辑地址转化为物理地址。
线性地址:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。
物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
图7.2-1 逻辑地址到线性地址
流程:通过段选择符的T1字段,确定是GDT中段还是LDT中的段。查找段描述符,获得基地址,基地址+偏移,得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(VA)到物理地址(PA)之间的转换通过对虚拟地址内存空间进行分页的分页机制完成。
图7.3-1 线性地址到物理地址1
图7.3-2 线性地址到物理地址2
MMU 利用 VPN 来选择适当的 PTE,将页表条目中的物理页号和虚拟地址中的 VPO 串联起来,就得到相应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
图7.4-1 VA-PA
如图所示为四级页表的VA到PA的转换。CR3控制寄存器指向第一级页表(L1)的起始位置。CR3的值是每个进程上下文的一部分,每次上下文切换时,C3的值都会被恢复。
36位VON被划分为了9位的片,每个片被用做到一个页表的偏移量。CR3寄存器包含L1也标的物理地址。VPN1提供到一个L1PET的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2PTE的偏移量,以此类推。
而VPO可以直接转化为VPO从而和PPN结合然后转化为物理地址。
7.5 三级Cache支持下的物理内存访问
获得物理地址之后,先取出组索引对应位,在L1中寻找对应组。如果存在,则比较标志位,相等后检查有效位是否为1.如果都满足则命中取出值传给CPU,否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后再一级一级向上传,如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的位置。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存,创建当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
(1)删除已存在的用户区域
删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域
为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
(3)映射共享区域
hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC)
execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。如图所示:
图7.7-1 execve
7.8 缺页故障与缺页中断处理
当要使用的虚拟地址对应的PTE的有效位为0时,说明该页面没有缓存到主存中,就发生了缺页异常,于是调用缺页异常处理程序。
缺页处理程序会选择一个牺牲页,若其内容发生改变,将其复制到磁盘中,更新PTE,将牺牲页的PTE的地址字段更新为牺牲页在磁盘中的起始地址,有效位置为0,将缺少的页的PTE的地址字段更新为缓存页的起始位置,也就是对应的PPN,将有效位置为1,缺页处理程序返回。
返回到导致缺页异常的指令再次运行,重新进行地址翻译。
图7.8-1 缺页
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。已分配的块显式地保留为供应用程序使用。空闲块保持空闲,直到它显式地被应用所分配。具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。
(1) 显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
(2) 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
7.9.1隐式链表
堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。
7.9.2显式链表
在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。
7.9.3带边界标记的合并
采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。通过双界标记,我们可以在常数时间内完成空闲块的合并,减小性能消耗。
7.9.4分离的空闲链表
维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射,hello进程execve时的内存映射、缺页故障与缺页中断处理和动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m字节的序列:
B0,B1,B2……Bm
所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行:
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix IO接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的内存资源。
Unix IO 函数:
(1)read 函数原型:
ssize_t read(int fd,void *buf,size_t count)
成功返回读取的字节数;出现错误则返回 -1 ;读取到文件末尾时则返回 0如果在调 read 之前已到达文件末尾,则这次 read 返回 0
(2)write 函数原型:
ssize_t write (int fd,const void * buf,size_t count);
成功会返回实际写入的字节数。当有错误发生时则返回 -1,错误代码存入 errno 中。
(3) open 函数:打开一个已存在的文件或者创建一个新文件的
函数原型:int open(char *filename, int flags, mode_t mode);
open 函数将filename 转换为一个文件描述符,并且返回描述符数字.返回的描述符总是在进程中当前没有打开的最小描述符.flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位.返回:若成功则为新文件描述符,若出错为-1.
(4)close 函数:关闭一个打开的文件.
函数原型:int close(int fd);
返回:若成功则为0, 若出错则为-1.
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程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
同时printf也调用了外部函数vsprintf(buf, fmt, arg)
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);
}
然后让我们追踪下write:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
这里是给几个寄存器传递了几个参数,然后一个int结束。将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址。
再来看看sys_call的实现:
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将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
完成了printf。
8.4 getchar的实现分析
int getchar(void){
static char buf[size];
static char* bb=buf;
If(n==0){
n=read(0,buf,size);
bb=buf;
}
return(--n>=0)? (unsigned char) *bb++ :EOF;
}
当使用getchar时,程序发生陷阱的异常。当按键盘时会产生中断。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,其中着重分析了printf 函数和 getchar 函数。通过这些知识点,加深了对IO的理解。
(第8章1分)
结论
1.由高级语言写出hello.c文件。
2.经过预处理,hello.c将.h库函数添加进了文件生成hello.i。
3.经过编译器编译,生成了hello.s,内容变味了汇编语言。
4.经过汇编器生成了hello.o文件,成为了可重定位文件。
5.通过连接器,写入其他可重定位文件成为可执行文件。
6. 在shell中按要求输入并执行hello可执行文件,shell fork()一个子进程然后execve,加载运行hello。
7.创建虚拟内存,映射到物理内存,开始执行。
8.hello运行中的,可能会收到ctrl-z,ctrl-c等信号,要调用信号处理程序对其进行处理。
9.printf会用到malloc函数申请堆空间,并且用IO管理设备进行输出。
10.最后,进程结束被回收。Hello结束。
感想:计算机系统从高级语言到最底层的硬件结构环环相扣,倘若只知道高级语言而不知整个程序的运作流程,那么所编写的程序一定不会做到效率最高,空间最省。必须要从整体上对计算机系统进行把握,才能够快速高效准确地解决问题。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
Hello.c 源文件
Hello.i 预处理后的文件
Hello.s 编译后的汇编文件
Hello.o 汇编后的可重定位文件
Hello 链接后的可执行文件
Hello22.txt hello.o的elf文件
Hello11.txt hello的elf文件
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1]百度百科_全球领先的中文百科全书 百度百科
[2]什么是shell _mmquit的博客-CSDN博客 csdn
[3]c程序语言的基本单位是什么,预处理是什么意思_Herizack的博客-CSDN博客
[4]shell内部命令和外部命令_小米渣ok的博客-CSDN博客
[5]深入理解计算机系统原书第3版-文字版.pdf
(参考文献0分,缺失 -1分)