计算机系统
大作业
题 目 程序人生-Hello’s
P2P
专 业 计算机科学与技术
学 号 L180301002
班 级 1803010
学 生 韩升周
指 导 教 师 史先俊
计算机科学与技术学院
2019年12月
摘要
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文dd的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
关键词:关键词1;关键词2;……;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第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 -
第1章 概述
1.1 Hello简介
第一步,向电脑输入代码并存储成hello.c文件。
第二步,将文件中的代码通过预处理形成hello.i、编译形成hello.s、汇编形成hello.o,然后链接使之成为可执行目标文件。
第三步,shell接收用户命令并使用fork函数创建子进程,接着在子进程中使用execve函数将hello.c加载到内存中、使用mmap函数将hello.c文件映射进内存并实现进程间共享内存,同时生成代码段、数据段以及堆栈段,调度器为hello.c文件分配时间片。
第四步,CPU通过生成一个虚拟地址访问主存,同时,CPU中的TLB、4级页表以及CPU和主存之间的三级Cache加速访问内存。在MMU中进行地址翻译之后,虚拟地址转换为物理地址,然后可以从内存中取指、译码并执行。中间使用了众多信号处理命令和函数。
经过以上四步hello由program转为process。
最后,在程序执行完毕之后,shell回收子进程,又变为未执行hello之前,即020。
1.2 环境与工具
Code blocks, VMWare, ubuntu, edb, gdb, gcc
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件 作用
Hello.c 源程序文件
Hello.i
源代码经过预处理后得到的ascii码文件
Hello.s
.i文件经过编译得到的汇编代码文件
Hello.o
汇编后的二进制目标文件
1.4 本章小结
本章主要描述了如何将一个.c文件经过处理后打印到屏幕上供用户观看的大致过程,其中涉及到了计算机内十分重要的几个硬件——CPU、RAM、IO等等,向我们展示了计算机的工作是如何进行的。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念: 在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
作用:扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define
声明指定的宏。
首先,在hello.i文件中插入了所有用#include命令指定的文件,包括stdlib.h、stdio.h、unistd.h三个文件。其次,删除了注释等信息。
2.4 本章小结
本章主要讲述预处理这一执行源程序的第一个步骤。通过观察.c文件经预处理后得到的.i文件理解了预处理的作用——扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:把高级语言变成计算机可以识别的2进制语言。
作用:产生汇编代码。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1 数据
常量:以下常量存放在只读数据域中
全局变量:存放在.data段,hello中无全局变量
局部变量:存储在堆栈段,如当前代码中的i,argv,argc
如图中的-20(%rbp)中存的就是argc
图中存的是i
3.3.2赋值
将i赋值为0
3.3.3类型转换
调用atoi函数将字符串转为整型
3.3.4算术操作
对局部变量i累加
3.3.5 关系操作
判断argc与4是否相等
判断i是否小于等于7
3.3.6 数组/指针/结构操作
对argv数组进行操作,数组在存储空间中申请的是连续的内存。
3.3.7 函数操作
终止程序
从键盘输入内读取字符串
打印到展幕
字符串转为整型
体眠
清除回车
3.4 本章小结
本章具体分析了编译(由.i到.s)的过程,对生成的汇编代码进行分析,了解到了C语言的数据在汇编代码中的存储位置的不同以及操作在汇编代码中的实现。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:可以被CPU直截执行的编程语言
作用:生成二进制目标代码文件
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
ELF格式
ELF头
段头部表
.init
.text
.rodata
.data
.bss
.symtab
.debug
.line
.strtab
可重定位条目:
.rela.text是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改,调用本地函数的指令则不需要修改。
offset表示需要进行重定向的代码在.text或.data节中的偏移位置,Type表示着该符号的重定义类型:是相对地址引用还是绝对地址引用。Addend是重定位时需要的偏移调整。
4.4 Hello.o的结果解析
机器语言的构成:由机器指令集构成。
与汇编语言的映射关系:一一对应。
操作数的不一致:
前者是hello.o的反汇编代码,后者是hello.s。能明显看出前者的操作数是16进制,而后者是10进制。
分支转移函数调用:
hello.o中,跳转位置不再是hello.s中的符号,而是地址。同时给出地址偏移量。
4.5 本章小结
本章主要分析了汇编(.s到.o)的过程、.s与.o的主要不同点——操作数的不一致以及分支转移函数调用处的参数的不同。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:将各种代码和数据片段收集并组合成为一个单一文件的过程。
作用:使得生成的文件可以被加载到内存并执行。
链接的概念与作用
通过链接器,将程序调用的外部函数(.o文件)与当前.o文件以某种方式合并,并得到./hello可执行目标文件的的过程成为链接。且该二进制文件可被加载到内存,并由系统执行。
链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。基于此特性的改进,以提高程序运行时的时间、空间利用效率。
链接是由叫做链接器的程序执行的。链接器使得分离编译成为可能。它将巨大的源文件分解成更小的模块,易于管理。我么可以通过独立地修改或编译这些模块,并重新链接应用,不必再重新编译其他文件。
5.2 在Ubuntu下链接的命令
使用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
5.3 可执行目标文件hello的格式
ELF头
段头部表
.init
.text
.rodata
.data
.bss
.symtab
.debug
.line
.strtab
节头部表
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
以.rodata段为例,它的size是0x3b,说明其大小是59个字节,它的Address是 0x402000, 这是它的起始地址,它的offset是0x2000,说明.rodata段相对于该文件起始的偏移地址为0x2000。
5.4 hello的虚拟地址空间
.init段:
.text段:
.rodata段:
.data段:
5.5 链接的重定位过程分析
一、hello与hello.o的不同
1、hello中加入了许多除.text外的节,而hello.o中只有.text节
2、hello由于与动态链接库中的库函数链接上,故比hello.o多了许多函数。
二、链接的过程
符号解析、重定位。
三、重定位
第一步,重定位节和符号定义。链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成一个节,这个节成为输出的可执行目标文件的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。
第二步,重定位节中的符号引用。链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
5.6 hello的执行流程
5.7 Hello的动态链接分析
dl_init之前:
dl_init之后:
5.8 本章小结
本章具体分析了链接的作用——将各种代码和数据片段收集并组合成为一个单一文件,链接的过程——符号解析和重定位。深入理解了重定位的过程以及作用。了解了可执行目标文件与.o文件的反汇编代码的不同。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:一个执行中程序的实例。
广义上:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
作用:每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。
一个独立的逻辑控制流,它提供一个假象,好像我们的每个程序都独占的使用处理器。
6.2 简述壳Shell-bash的作用与处理流程
作用:接收用户命令,然后调用相应的应用程序。
处理流程:
1.从脚本或终端或bash -c选项后的字符串中获取输入
2.将获取的输入分解成词元,此步骤会执行别名展开
3.将词解析为简单命令或复合命令
4.执行各种shell展开
5.执行必要的重定向,
6.执行命令
如果命令中包含“/”,则执行制定路径的程序;如果命令中不包含“/”,会检查是否是shell函数,shell内建命令,如果都不是,则在PATH环境变量中的路径进行查找。
7.等待命令结束获取命令执行状态.
6.3 Hello的fork进程创建过程
子进程得到与父进程用户级虚拟地址空间相同但独立的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本。当父进程调用fork时,子进程可以读写父进程中打开的任何文件。
6.4 Hello的execve过程
execve的函数原型为:int
execve(char *filename,char *argv[],char *envp[])
execve函数加载并运行可执行目标文件filename,argv是其参数列表,envp是环境变量列表。令hello作为其第一个参数,使其可以运行hello程序。执行时其会删除当前子进程现有的虚拟内存段,创建一组新的段,其中新的代码段与数据段初始化为可执行文件hello的内容,实现覆盖(但还保留相同的PID),堆与栈初始化为0,同时将虚拟空间中的页映射到可执行文件对应内容实现页分配。之后调用启动代码,启动代码设置栈,将控制传递给hello程序的主函数,之后在具体执行hello的指令时由于未映射到物理内存会触发缺页中断并通过处理程序加载运行。
6.5 Hello的进程执行
操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文。它由一些对象的值组成,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策叫做调度。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。
6.6 hello的异常与信号处理
6.7本章小结
本章具体分析了进程管理,理解了fork函数以及execve函数在进程执行过程中的作用。同时通过运行进程时输入的各种命令——ps、pstree、crtl+c、crtl+z、kill、fg等等了解了进程管理。 本章介绍了Hello程序是如何在shell中运行的:以进程形式。介绍了shell-bash的作用和一般处理流程。介绍了进程中两个关键的抽象:逻辑控制流和私有空间。并且通过分析hello进程的创建,加载和运行进一步介绍了上下文切换的机制,通过运行hello程序,介绍了异常控制流的通知机制信号。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:访问指令给出的地址。
线性地址:段地址+偏移地址。
虚拟地址:程序访问存储器所使用的逻辑地址。
物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
实模式下: 逻辑地址CS:EA =物理地址CS*16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址,
段地址+偏移地址=线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
分页管理机制通过上述页目录表和页表实现32位线性地址到32位物理地址的转换。控制寄存器CR3的高20位作为页目录表所在物理页的页码。首先把线性地址的最高10位(即位22至位31)作为页目录表的索引,对应表项所包含的页码指定页表;然后,再把线性地址的中间10位(即位12至位21)作为所指定的页目录表中的页表项的索引,对应表项所包含的页码指定物理地址空间中的一页;最后,把所指定的物理页的页码作为高20位,把线性地址的低12位不加改变地作为32位物理地址的低12位。
7.4 TLB与四级页表支持下的VA到PA的变换
1、CPU产生一个虚拟地址,并把它传送给MMU。
2、MMU生成PTE地址,并从告诉缓存/主存请求得到它。
3、高速缓存/主存向MMU返回PTE。
4、MMU构造物理地址,并把它传送给高速缓存/主存。
5、高速缓存/主存返回所请求的数据字给处理器。
7.5 三级Cache支持下的物理内存访问
CPU产生一个虚拟地址并发送给MMU,如果命中则发送给一级Cache,如果没有命中则回页表中查找,然后一级一级重复上述步骤。
7.6 hello进程fork时的内存映射
fork函数为子进程创建虚拟内存,创建子进程的mm_struct ,vm_area_struct和页表的原样副本。同时将父进程与子进程两个进程中的每一个页面都标记为只读。同时将两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制。此时它们映射到相同的物理地址,但物理地址作为它们的私有写时复制对象,在对共享物理页进行写操作时会触发保护故障从而实现写时复制,创建新的页面,写的部分映射为不同部分。
7.7 hello进程execve时的内存映射
1、删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在`的区域结构。
2、映射私有区域。为新程序的代码、数据、bss和栈区创建新的区域结构
3、映射共享区域。
4、设置程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
1、搜索区域结构的链表,把虚拟地址和每个区域结构中的vm_start和vm_end作比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。
2、判断进程是否有读、写或者执行这个区域内页面的权限。如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。
3、到这一步,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。此时,缺页处理程序选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送虚拟地址到MMU。
7.9动态存储分配管理
动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的 块(blocks)的集合来维护,每个块要么是已分配的,要么是空闲的。
函数原型:void *malloc(size_t size)
成功:
返回已分配块的指针,块大小至少 size 字节,对齐方式依赖编译模式:8字节(32位模式),16字节(64位模式)
If size == 0,
returns NULL
出错:返回 NULL (0) ,同时设置 errno
目标:最大化吞吐量,最大化内存利用率
策略:利用首次适配、下一次适配和最佳适配放置空闲块;分割空闲块;合并空闲块。
7.10本章小结
要想执行hello程序,CPU给出一个逻辑地址,依据段式管理将其转化为线性地址,fork、execve函数可以使得hello可执行程序的各个区域映射到对应的虚拟内存。接着就是通过页式管理将虚拟地址转换为物理地址,如果页式管理仅仅依靠页表将是十分缓慢地,因此采用TLB进行加速翻译,又可以采用多级页表将页表进行压缩处理,当虚拟地址还未缓存至物理内存时又会触发缺页中断进行处理。通过一系列优化措施最终得到物理地址,终于可以结合3级cache访问物理内存。malloc使尽浑身解数为满足了hello中的各个请求,终于为它找到了合适的舞台能够登台演出。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。还是
一个 Liunx 文件就是一个 m 个字节的序列:B0,B1,…,Bm-1。所有的 I/O 设 备都被模型化为文件。
文件的类型有:
1.普通文件:包含任何数据,分两类
i.文本文件:只含有 ASCII 码或 Unicode 字符的文件
ii.二进制文件:所有其他文件
2.目录:包含一组链接的文件。每个链接都将一个文件名映射到一个文件
3.套接字:用于与另一个进程进行跨网络通信的文件
而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅的映射为文件的方式,允许 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)进程通过调用open函数打开一个已存在的文件或者创建一个新文件。open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有 打开的最小描述符;flags参数指明进程打算如何访问这个文件,这个参数也可以是一个或者更多位的掩码的或;mode参数指定新文件的访问权限位。
int close(int
fd)进程通过调用close函数关闭一个已打开的文件,关闭一个已关闭的描述符会出错。
size_t
read(int fd,void *buf,size_t n)
进程通过调用read函数执行输入功能。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,返回值0表示EOF。否则返回值表示实际传送的字节数量
函数:
1、open函数可以打开或者创建一个文件,函数原型如下
#include
int
open(const char *pathname,int oflag,…/mode_t mode/)
返回值:若成功返回文件描述符,若出错返回-1
pathname不用说指的是文件的路径名,注意使用’/'来间隔目录。
对于第二个参数oflag,以下是需要使用到fcntl.h中的常用参数:
O_RDONLY(只读)
O_WRONLY(只写)
O_RDWR(读写)
//以上这三个常量只能选择一个
O_APPEND(末尾追加)
O_CREAT(如果文件不存在则创建文件,这时需要使用第三个参数mode,指定该文件的访问权限位)
O_EXCL(如果文件存在,则出错,用来检测文件是否存在)
O_TRUNC(如果文件存在,而且为只读或只写打开,则将其长度截短为1)
对于open函数,当且仅当创建新文件时才使用的三个参数。由open返回的文件描述符一定是最小的未用的文件描述符,这可以用先关闭标准输出,然后打开文件,则文件的描述符为1。
2、creat函数
creat函数用来创建一个新文件,函数原型如下
#includ
int
creat(const *pathname,mode_t mode)
返回值:若成功则返回为只写打开的文件描述符,否则返回-1
等效于open(pathname.O_WRONLY | O_CREAT
| O_TRUNC,mode)
由于creat只能以写的方式打开文件,要想读取该文件需要creat,close,open。可以直接采用如下方式open(pathname,O_RDWR
| O_CREAT |O_TRUNC,mode)。
3、close函数
关闭文件并释放其所占资源,函数原型如下
#include
int
close(int filedes)
返回值:成功返回0,否则返回-1
4、lseek函数
每个打开的文件在内核中都维护了一个“当前文件偏移量”,指的是从文件开始处计算的字节数。除非使用O_APPEND,否则偏移量一般都为0。
调用lseek显示地为一个打开的文件设置偏移量,函数原型如下
#include
off_t
lssek(int filedes, off_t offset, int whence)
返回值:若成功则返回新的文件偏移量,若出错则返回-1
//若whence为SEEK_SET,则将文件的偏移量设置为距文件开始处offset个字节。////
//若whence为SEEK_CUR,则将文件的偏移量设置为当前值加offset,offset可正可负。
//若whence为SEEK_END,,则将文件的偏移量设置为文件长度加offse,offset可正可负,
//注意文件偏移量可以大于当前文件长度,系统并不会为之间的空洞分配磁盘空间,只为新的数据分配磁盘空间。
5、read函数
调用read函数从打开的文件中读取数据,函数原型如下:
#include
ssize_t
read(int filedes,void *buf,size_t nbytes)
返回值:若成功返回读到的字节数,若已到文件结尾则返回0,若出错返回-1
6、write函数
调用write函数向打开的文件写数据,函数原型如下:
#include
ssize_t
write(int filedes,const void* buff,size_t nbytes)
返回值:若成功返回已写的字节数,若出错返回-1
其返回值通常与参数nbytes相同,否则表示出错。常见的出错原因包括:磁盘已经写满、超过了文件的最大长度限制。对于普通文件,写操作从文件的当前偏移量处开始。执行一次成功的写之后,文件的偏移量增加实际写的字节数。
8.3 printf的实现分析
前提:printf 和 vsprintf 代码是 windows 下的。
查看 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;
}
首先 arg 获得第二个不定长参数,即输出的时候格式化串对应的值。
查看 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’: //只处理%x一种情况 itoa(tmp,
((int)p_next_arg)); //将输入参数值转化为字符串保存在tmp
strcpy(p, tmp); //将tmp字符串复制到p处 p_next_arg += 4; //下一个参数值地址
p += strlen(tmp); //放下一个参数值的地址 break;
case ‘s’:
break;
default:
break;
}
}
return (p - buf); //返回最后生成的字符串的长度
}
则知道 vsprintf 程序按照格式 fmt 结合参数 args 生成格式化之后的字符串,并 返回字串的长度。 在 printf 中调用系统函数 write(buf,i)将长度为 i 的 buf 输出。write 函数如下
write: mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在 write 函数中,将栈中参数放入寄存器,ecx 是字符个数,ebx 存放第一个字符地址,int INT_VECTOR_SYS_CALLA 代表通过系统调用 syscall,查看 syscall 的实现
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 将字符串中的字节“Hello L180300401 沈玟锡 2”从寄存器中通过总线复
制到显卡的显存中,显存中存储的是字符的 ASCII 码。
字符显示驱动子程序将通过 ASCII 码在字模库中找到点阵信息将点阵信息存 储到 vram 中。
显示芯片会按照一定的刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB 分量)。 于是我们的打印字符串 “Hello L180300401
沈玟锡”
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
为了让hello这个“倔脾气”能够和我们进行友好的交互,Unix I/O可谓是煞费苦心。Linux将所有I/O设备模型化为文件,所有的输入输出都被当作相应文件的读和写来执行,这使得所有输入和输出都以统一方式来执行。本章重点介绍了这种统一方式下的Unix I/O接口以及I/O函数,着重分析了printf以及getchar的实现。通过大家的重重努力,hello终于向我们说了第一句话!(I/O系统激动地热泪盈眶)
(第8章1分)
结论
程序员通过鼠标、键盘等I/O设备编写代码,hello.c诞生并存储在文件内。
预处理器对hello.c进行预处理,解释宏定义,插入系统头文件代码,修改hello.c源文件,形成hello.i。
编译器对hello.i文件进行编译,形成更接近机器代码的汇编代码,存储在hello.s文件中。
汇编器将汇编代码全部转换为机器代码,生成可重定位目标文件hello.o,hello就这样变成了我们的陌生人!
链接器将许多和hello相仿的小伙伴们结合起来,将每一个可重定位目标文件中的符号引用都能与一个确定地址的明确的符号定义关联起来,生成最终的可执行文件hello
通过I/O设备在shell中键入命令行./hello 1180300407 张高玮 10运行hello程序
shell通过fork()函数创建一个子进程,在子进程中通过execve函数加载hello程序,建立hello可执行文件到虚拟内存的映射
子进程运行到hello程序入口处,发生缺页中断
操作系统通过调用缺页异常处理子程序,将hello加载到物理内存中去
逐条执行hello的指令,CPU为hello进程分配了固定时间片,与其他进程并发执行
指令执行过程中需要访问内存,通过TLB、4级页表、3级cache的多重帮助、加速之下快速访问主存
程序员从键盘上键入Ctrl+C、Ctrl+Z,OS向hello进程发送相应信号,调用相应的信号处理子程序
printf函数调用malloc函数在堆上申请内存空间,从分离的空闲链表中寻找合适的空闲块将其分配
hello进程终止,向父进程shell发送SIGCHLD信号
父进程接收到hello发送的信号,将hello进程进行回收,清除其占用的空间与资源
深切感悟:整个计算机系统的设计与实现实在太过精巧,对于各种数据类型的运算都要保证准确地进行;对于稀缺的内存资源煞费苦心地用多种策略进行组织;对于从高级代码到低级的机器代码的转换有各种小技巧来实现性能的加速;对于效率的提高运用了多级缓存来保证···但与此同时,我也深刻体会到,计算机对于性能的追求要建立在绝对的正确性之上,那些即使只有小概率会犯错的加速技巧也是不被采用的。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
hello.c
源程序文件
hello.i
源代码经过预处理后得到的ascii码文件
hello.s
.i文件经过编译得到的汇编代码文件
hello.o
汇编后的二进制目标文件
hello
链接产生的文件
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴.
空间控制技术[M].
北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东.
新时代的工业工程师[M/OL].
台北:天下文化出版社,1998
[1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖.
空间交会控制理论与方法研究[D].
哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H.
Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in
the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23].
http://www.sciencemag.org/cgi/ collection/anatmorp.
(参考文献0分,缺失 -1分)