计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机大类
学 号 120L021501
班 级 2003012
学 生 曹煜浩
指 导 教 师 郑贵滨
计算机科学与技术学院
2021年5月
本文将通过对于hello.c在linux下是如何运行的论述,来带领我们了解程序在计算机系统的运行过程,并且让我们对计算机的奥妙之处有所了解。本文中,我们将详细介绍预处理,编译,汇编,链接,进程管理,存储管理,IO几部分,来让我们深入了解计算机系统。
关键词:编译;预处理;链接;进程管理;存储管理;IO;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.P2P:
from program to progress。我们将代码输入计算机,利用编译器将代码转化为程序,然后通过cpp、ccl、as、ld,将程序变为可执行程序。
2.020:
-
- shell为hello进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存。
- 进入 main 函数执行目标代码,CPU为运行的hello分配时间片执行逻辑 控制流。
- 当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据 结构。
1.2 环境与工具
硬件环境:Intel Core i7-6700HQ x64CPU,16G RAM,256G SSD.
软件环境:Ubuntu20.04.1 LTS
开发与调试工具:gedit,gcc,as,ld,gdb,readelf,HexEdit
1.3 中间结果
hello.i:cpp处理后的文件
hello.s:编译后的文件
hello.o:汇编后的可重定位目标执行
hello:可执行文件
hello.elf:hello.o的ELF格式
hello.objdump:hello.o反汇编代码
hello1.objdump:hello的反汇编代码
1.4 本章小结
本章主要简单介绍了 hello 的 p2p,020 过程,列出了本次实验信息:环境、中间结果
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。
预处理的过程:
- 将源文件中用#include 形式声明的文件复制到新的程序中。比如 hello.c第 6-8 行中的#include<stdio.h> 等命令告诉预处理器读取系统头文件stdio.h unistd.h stdlib.h 的内容,并把它直接插入到程序文本中。
- 用实际值替换用#define 定义的字符串
- 根据#if 后面的条件决定需要编译的代码
- 特殊符号,预编译程序可以识别一些特殊的符号,预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
cpp hello.c > hello.i
2.3 Hello的预处理结果解析
利用gedit打开hello.i,发现程序扩充到3060行,main函数从3047行开始。
在这一步中,头文件unistd.h,stdio.h,stdlib.h全部展开,cpp 到Ubuntu中默认的环境变量下寻找头文件,并在相应的地址处打开这些文件。拿stdio.h举例,切换到计算机下的usr/local/stdio.h,我们发现一件事情,stdio.h中依然有#define,而.i文件中是没有#define文件的,取而带之的是#ifndef,#ifdef的语句,由此得到的结论是cpp 会对条件值进行判断来决定是否执行包含其中的逻辑。特殊符号,预编译程序可以识别一些特殊的符号,预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.4 本章小结
一个简简单单的hello.c文件,所需要的代码非常多,甚至达到了近4000行。在这一章中,我利用cpp将hello.c变为了hello.i。理解了预处理器如何处理代码。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是指把用高级程序设计语言书写的源程序,翻译成等价的机器语言格式目标程序的翻译程序。编译程序属于采用生成性实现途径实现的翻译程序。它以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。编译出的目标程序通常还要经历运行阶段,以便在运行程序的支持下运行,加工初始数据,算出所需的计算结果。
编译程序的作用是将高级程序语言源程序翻译为目标程序。编译程序属于采用生成性实现途径实现的翻译程序。它以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。
以下为编译的基本流程:
- 语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法。
- 中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
- 代码优化:指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。
- 目标代码:生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,须经过汇编程序汇编后,成为可执行的机器语言代码。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.3.1
指令 | 含义 |
.file | 声明源文件 |
.text | 以下是代码段 |
.section .rodata | 以下是 rodata 节 |
.globl | 声明一个全局变量 |
.type | 用来指定是函数类型或是对象类型 |
.size | 声明大小 |
.long、.string | 声明一个 long、string 类型 |
.align | 声明对指令或者数据的存放地址进行对齐的方式 |
3.3.2
hello.s中用到的数据类型有整数,字符串,数组。
一、字符串:
"Hello 120L021501 \346\233\271\347\205\234\346\265\251 \347\247\222\346\225\260\357\274\201"
"Hello %s %s\n"
"GNU"
其中,第一个字符串是在第一个printf中打印的内容
printf("Hello 120L021501 曹煜浩 秒数!\n");
该字符串被编译成了UTF-8格式一个数字一个字节,一个汉字在 utf-8 编码中占三个字节,一个\代表一个字节。
第二个字符串是在第二个printf中打印的内容
printf("Hello %s %s\n",argv[1],argv[2]);
后两个字符串都声明在了.rodata 只读数据节。
第三个字符串表明使用的是GNU。
- 整数:
(1)int i,
for(i=0;i<8;i++){
.L2:
movl $0, -4(%rbp)
jmp .L3
可以看出,这是将一个4字节的数据压入栈中
- int argc,作为第一个参数传入
- 立即数,类似于$32这种
- 数组
- char *argv[]
这个是main函数执行时输入的命令行,argv 单个元素 char*大小为 8字节,argv 指针指向存放着字符指针的连续空间,起始地址为 argv。main 函数中访问数组元素argv[1],argv[2]时,按照起始地址 argv 大小8B计算数据地址取数据,在hello.s 中,使用两次(%rax)(两次 rax 分别为 argv[1]和 argv[2]的地址)取出其值。第一次是在语句movq (%rax), %rdx,第二次是在语句movq (%rax), %rax。
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
3.3.3、数据类型
数据的赋值:
这里涉及到的数据赋值是i=0,其中在汇编代码中的指令为movl $0, %eax,因为i的数据类型为int,所以使用的是%eax.
3.3.4、算术操作
涉及的算术操作:
指令 | 算术表达式 |
leaq a,b | b=&a |
inc a | a+=1 |
dec a | a-=1 |
neg a | a=-a |
add a,b | b=b+a |
sub a,b | b=b-a |
imulq a | r[%rdx]:r[%rax]=a*r[%rax](有符号) |
mulq a | r[%rdx]:r[%rax]=a*r[%rax](无符号) |
idivq a | r[%rdx]=r[%rdx]:r[%rax] mod a(有符号) r[%rax]=r[%rdx]:r[%rax] div a |
divq a | r[%rdx]=r[%rdx]:r[%rax] mod a(无符号) r[%rax]=r[%rdx]:r[%rax] div a |
程序中涉及到的算术操作:
- i++
addl $1, -4(%rbp)
- 汇编中使用leaq .LC1(%rip),%rdi,使用了加载有效地址指令leaq计算LC1 的段地址%rip+.LC1并传递给%rdi。
leaq .LC1(%rip), %rdi
3.3.5 关系操作
指令 | 意义 | 描述 |
CMP S1,S2 | S2-S1 | 比较-设置条件码 |
TEST S1,S2 | S1&S2 | 测试-设置条件码 |
SET** D | D=** | 按照**将条件码设置 |
D J** | —— | 根据**与条件码进行跳转 |
该程序中涉及的关系运算:
- argc!=4。判断argc和4是否相等。具体在代码中是cmpl $4, -20(%rbp)
- i<8,具体在代码中体现是cmpl $7, -4(%rbp)。
3.3.5、控制转移
(1)if控制语句,当argc!=4的时候执行该语句。
if(argc!=4){
printf("Hello 120L021501 曹煜浩 秒数!\n");
exit(1);
}
汇编代码为
cmpl $4, -20(%rbp)
je .L2
对于if判断,可以通过跳转指令实现。利用cmpl比较argv和4,设置条件码,使用je判断ZF标志位,如果为0,说明argv-4=0,则不执行if中的代码直接跳转到.L2,否则顺序执行执行if中的代码。
(2)for循环,让变量循环8次
for(i=0;i<8;i++){
printf("Hello %s %s\n",argv[1],argv[2]);
sleep(atoi(argv[3]));
}
如果i<8,则执行跳转功能,继续循环,不满足条件,跳出循环。
3.3.6、函数操作
C语言中,子程序的作用是由一个主函数和若干个函数构成。由主函数调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多个函数调用任意多次。在程序设计中,常将一些常用的功能模块编写成函数,放在函数库中供公共选用。要善于利用函数,以减少重复编写程序段的工作量。
函数包括如下内容:
- 函数表达式:函数作为表达式中的一项出现在表达式中,以函数返回值参与表达式的运算。这种方式要求函数是有返回值的。
- 函数语句:函数调用的一般形式加上分号即构成函数语句。
- 函数实参:函数作为另一个函数调用的实际参数出现。这种情况是把该函数的返回值作为实参进行传送,因此要求该函数必须是有返回值的。
调用函数的动作如下:
- 传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。
- 传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回一个值。
- 分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
64 位程序参数存储顺序:
1 | 2 | 3 | 4 | 5 | 6 | 7 |
%rdi | %rsi | %rdx | %rcx | %r8 | %r9 | 栈空间 |
浮点数使用 xmm
该程序中涉及到的函数:
- main函数
main:
.LFB6:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $4, -20(%rbp)
je .L2
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
- 传递控制,main 函数因为被调用 call 才能执行(被系统启动函数__libc_start_main 调用),call 指令将下一条指令的地址 dest 压栈,然后跳转到 main 函数。
- 传递数据,外部调用过程向 main 函数传递参数 argc 和 argv,分别使用%rdi 和%rsi 存储,函数正常出口为 return 0,将%eax 设置 0返回。
- 分配和释放内存,使用%rbp 记录栈帧的底,函数分配栈帧空间在%rbp 之上,程序结束时,调用 leave 指令,leave 相当于mov %rbp,%rsp,pop %rbp,恢复栈空间为调用之前的状态,然后 ret返回,ret 相当 pop IP,将下一条要执行指令的地址设置为 dest。
- printf函数
相关代码,call puts@PLT
- 传递数据:
第一次 printf 将%rdi 设置为“Usage: Hello 学号 姓名!\n”字符串的首地址。第二次 printf 设置%rdi 为“Hello %s %s\n”的首地址,设置%rsi 为 argv[1],%rdx 为 argv[2]。
- 控制传递:
第一次 printf 因为只有一个字符串参数,所以 callputs@PLT;第二次 printf 使用 call printf@PLT。
- exit 函数:
call exit@PLT
- 传递数据:将%edi 设置为 1。
- 控制传递:call exit@PLT。
- sleep函数
call sleep@PLT
- 传递数据:将%edi 设置为 sleepsecs。
- 控制传递:call sleep@PLT
- getchar函数
call getchar@PLT
控制传递:call gethcar@PLT
3.4 本章小结
本章主要阐述了编译器处理 C 语言的各个数据类型的过程,阐述了从 hello.c 程序到 hello.s 汇编代码之间的映射关系。编译器将.i 的拓展程序编译为.s 的汇编代码,以方便机器进行进一步的处理。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编程序是指把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。汇编语言是为特定计算机或计算机系列设计的一种面向机器的语言,由汇编执行指令和汇编伪指令组成。采用汇编语言编写程序虽不如高级程序设计语言简便、直观,但是汇编出的目标程序占用内存较少、运行效率较高,且能直接引用计算机的各种设备资源。它通常用于编写系统的核心部分程序,或编写需要耗费大量运行时间和实时性要求较高的程序段。
汇编语言是二进制指令的文本形式,与指令是一一对应的关系。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
readelf -a hello.o > hello.elf
ELF组成如下:
- ELF头:从16字节的序列magic开始,Magic 描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。
- 结头:节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。
- 重定位节.rela.text,一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。8 条重定位信息分别是对.L0(第一个 printf 中的字符串)、puts 函数、exit 函数、.L1(第二个 printf 中的字符串)、printf 函数、sleepsecs、sleep 函数、getchar 函数进行重定位声明。
.rela节的内容
offset | 需要进行重定向的代码在.text或.data节中的偏移位置,8个字节。 |
Info | 包括symbol和type两部分,其中symbol占前4个字节,type占后4个字节,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型 |
Addend | 计算重定位位置的辅助信息,共占8个字节 |
Type | 重定位到的目标的类型 |
Name | 重定向到的目标的名称 |
下面以.L1 的重定位为例阐述之后的重定位过程:链接器根据 info 信息向.symtab 节中查询链接目标的符号,由 info.symbol=0x05,可以发现重定位目标链接到.rodata 的.L1,设重定位条目为 r,
r 的构造为:r.offset=0x18, r.symbol=.rodata, r.type=R_X86_64_PC32, r.addend=-4
重定位一个使用 32 位 PC 相对地址的引用。计算重定位目标地址的算法如下(设需要重定位的.text 节中的位置为 src,设重定位的目的位置 dst):
refptr = s +r.offset (1)
refaddr = ADDR(s) + r.offset (2)
*refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr)(3)
其中(1)指向 src 的指针(2)计算 src 的运行时地址,(3)中,
ADDR(r.symbol)计算 dst 的运行时地址,在本例中,ADDR(r.symbol)获得的是 dst 的运行时地址,因为需要设置的是绝对地址,即 dst 与下一条指令之间的地址之差,所以需要加上 r.addend=-4。之后将 src 处设置为运行时值*refptr,完成该处重定位。
- 符号表目标文件的符号表中包含用来定位、重定位程序中符号定义和引用的信息。符号表索引是对此数组的索引。索引 0 表示表中的第一表项,同时也作为定义符号的索引。
- 程序头部表,ELF文件头结构就像是一个总览图,描述了整个文件的布局情况。因此在ELF文件头结构允许的数值范围内,整个文件的大小是可以动态增减的。如果存在的话,告诉系统如何创建进程映像。
有图像知,该程序不存在程序头
- 节头表,通过目标文件中的节头表,我们就可以轻松地定位文件中所有的节。节头表是若干结构Elf32_Shdr或者结构Elf64_Shdr组成的数组。节头表索引(section header table index)是用来定位节头表的表项的。ELF文件头中的e_shoff代表的是节头表在文件中的偏移值;e_shnum代表的是节头表的表项总数;e_shentsize代表的是节头表的表项大小。
由图像知,该程序没有节头表
- Dynamic section,如果目标文件参与动态链接,则其程序头表将包含一个类型为 PT_DYNAMIC 的元素。此段包含 .dynamic 节。特殊符号 _DYNAMIC 用于标记包含以下结构的数组的节
有图像知,还是没有
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
hello.s与objdump得到的文件主要差异如下:
-
- 分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L1,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
- 函数调用:在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编序中,call 的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其 call 指令后的相对地址设置为全 0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。
- 全局变量访问:在.s 文件中,访问 rodata(printf 中的字符串),使用段名称+%rip,在反汇编代码中 0+%rip,因为 rodata 中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全 0 并添加重定位条目。
4.5 本章小结
在这一章中,我了解了hello.s到hello.o的过程,了解了hello的ELF格式,并且将objdump与hello.s进行了对比,了解了机器代码与汇编语言的转换。
(第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
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
利用readelf -a hello > hello.elf生成hello.elf。
- 在 ELF 格式文件中,Section Headers 对 hello 中所有的节信息进行了声明,其中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address是程序被载入到虚拟地址的起始地址。
- ELF Header:以16B的序列Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。
与链接前的ELF header比较,可见除系统决定的基本信息不变外,section header和program header均增加,并获得了入口地址。
- 程序头部表(Program Header Table),ELF文件头结构就像是一个总览图,描述了整个文件的布局情况。因此在ELF文件头结构允许的数值范围内,整个文件的大小是可以动态增减的。告诉系统如何创建进程映像。
段映射(segment mapping),映射操作仅适用于本机架构互连,如Dolphin-SCI或NewLink。映射段授予CPU内存操作访问该段的权限,从而节省调用内存访问原语的开销。
- Dynamic section,如果目标文件参与动态链接,则其程序头表将包含一个类型为 PT_DYNAMIC 的元素。此段包含 .dynamic 节。特殊符号 _DYNAMIC 用于标记包含以下结构的数组的节
- 重定位节.rela.text ,一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。六条重定位信息,分别描述了原hello的函数main、标准头文件的函数puts、函数printf、函数getchar、函数exit、函数sleep的重定位声明
- 符号表节(symbol table),目标文件的符号表包含定位和重定位程序的符号定义和符号引用所需的信息。符号表索引是此数组的下标。索引 0 指定表中的第一项并用作未定义的符号索引。
动态符号表 (.dynsym) 用来保存与动态链接相关的导入导出符号,不包括模块内部的符号。
其中 .symtab 段保存函数名和变量名等基本的符号的地址和长度等
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
在0x400000~0x401000段中,程序被载入,自虚拟地址 0x400000 开始,自0x400fff 结束,这之间每个节(开始 ~ .eh_frame 节)的排列即开始结束 Address 中声明。
查看 ELF 格式文件中的 Program Headers,程序头表在执行的时候被使用,告诉链接器运行时加载的内容并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐方面的信息。
在下面可以看出,程序包含8个段:
-
- PHDR 保存程序头表。
- INTERP 指定在程序已经从可执行文件映射到内存之后,必须调用的解释器(如动态链接器)。
- LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串)、程序的目标代码等。
- DYNAMIC 保存了由动态链接器使用的信息。
- NOTE 保存辅助信息。
- GNU_STACK:权限标志,标志栈是否是可执行的。
- GNU_RELRO:指定在重定位结束之后那些内存区域是需要设置只读。
通过Data Dump查看虚拟地址段0x600000~0x602000,在0~fff空间中,与0x400000~0x401000段存放的程序相同,在fff之后存放的是.dynamic~.shstrtab节。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
通过指令objdump -d -r hello > hello1.objdump获取hello的反汇编文件
与 hello.o 反汇编文本 hello.objdump 相比,在 hello1.objdump 中多了许多节,列在下面。
节名称 | 描述 |
.interp | 保存 ld.so 的路径 |
.note.ABI-tag | Linux 下特有的 section |
.hash | 符号的哈希表 |
.gnu.hash | GNU 拓展的符号的哈希表 |
.dynsym | 运行时/动态符号表 |
.dynstr | 存放.dynsym 节中的符号名称 |
.gnu.version | 符号版本 |
.gnu.version_r | 符号引用版本 |
.rela.dyn | 运行时/动态重定位表 |
.rela.plt | .plt 节的重定位条目 |
.init | 程序初始化需要执行的代码 |
.plt | 动态链接-过程链接表 |
.fini | 当程序正常终止时需要执行的代码 |
.eh_frame | 包含异常展开和源语言信息。 |
.dynamic | 存放被 ld.so使用的动态链接信息 |
.got | 动态链接-全局偏移量表-存放变量 |
.got.plt | 动态链接-全局偏移量表-存放函数 |
.data | 初始化了的数据 |
.comment | 包含编译器的NULL-terminated字符串 |
通过比较 hello.objdump 和 helloo.objdump 了解链接器。
-
- 函数个数:在使用ld命令链接的时候,指定了动态链接器为 64 的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了 hello.c 中用到的 printf、sleep、getchar、exit 函数和_start 中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main。链接器将上述函数加入。
- 函数调用:链接器解析重定条目时以类型R_X86_64_PLT32对外部函数调用重定位,此时PLT中已经加入到动态链接库中的函数,链接器根据已经确定的.text与.plt 节的相对距离计算相对距离,将对动态链接库中函数的调用值改为 PLT 中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt 与.got.plt。(链接器根据相对距离修改调用值)
- .rodata 引用:链接器解析重定条目时以R_X86_64_PC32类型对.rodata 两个重定位(printf 中的两个字符串),确定.rodata 与.text 节之间的相对距离,call之后的值被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的字符串。(取相对距离确定相对距离)
以计算第一条字符串相对地址为例说明计算相对地址的算法:
refptr = s + r.offset = Pointer to 0x40054A
refaddr = ADDR(s) + r.offset= ADDR(main)+r.offset=0x400532+0x18=0x40054A
*refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr) =ADDR(str1)+r.addend-refaddr=0x400644+(-0x4)-0x40054A=(unsigned) 0xF6,
观察反汇编验证计算:
40113e: 48 8d 3d c3 0e 00 00 lea 0xec3(%rip),%rdi # 402008 <_IO_stdin_used+0x8>
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
程序名称 | 程序地址 |
_start | 0x00000000004010f0 |
__libc_csu_init | 0x0000000000401270 |
_init () | 0x0000000000401000 |
frame_dummy () | 0x00000000004011d0 |
register_tm_clones () | 0x0000000000401160 |
main () | 0x00000000004011d6 |
puts@plt | 0x0000000000401090 |
exit@plt | 0x00000000004010d0 |
__do_global_dtors_aux | 0x00000000004011a0 |
deregister_tm_clones | 0x0000000000401130 |
_fini () | 0x00000000004012e8 |
hello!puts@plt | 0x00000000004012e8 |
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
由于无法预测函数的运行时地址,对于动态共享链接库中 PIC 函数,编译器需要添加重定位记录,等待动态链接器处理。链接器采用延迟绑定的策略,防止运行时修改调用模块的代码段。
动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在dl_init调用之前,0x601008 和 0x601010 处的两个 8B 数据为空。对于每一条PIC函数调用,调用的目标地址都实际指向PLT 中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
5.8 本章小结
本章主要描绘了链接的过程,通过本章的学习,我了解到了链接的过程都发生了哪些变化。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。
进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell 的作用:在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(命令解析器)。它类似于DOS下的command.com和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。Shell 是一个用 C 语言编写的程序,他是用户使用 Linux 的桥梁。Shell 是指一种应用程序,Shell 应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
-
- 从终端读入输入的命令。
- 将输入字符串切分获得所有的参数
- 判断是否是内置命令,如果是内置命令则立即执行
- 如果不是,则调用相应的程序为其分配子进程并运行
- shell 应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
在终端Terminal中键入./hello,运行的终端程序会对输入的命令行进行解析。
- hello 不是一个内置的shell命令所以解析之后终端程序判断./hello 的语义为执行当前目录下的可执行目标文件 hello。
- 之后终端程序首先会调用 fork 函数创建一个新的运行的子进程,新创建的子进程几乎父进程相同,但不完全与相同。
- 父进程与子进程之间最大的区别在于它们拥有不同的 PID。子进程得到与父进程用户级虚拟地址空间相同的一份副本,当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。
- 内核能够以任意方式交替执行父子进程的逻辑控制流的指令,父进程与子进程是并发运行而独立的。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
具体流程:
6.4 Hello的execve过程
- 为子进程调用函数fork之后,子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序hello。
- 为执行hello程序加载器、删除子进程现有的虚拟内存段,execve 调用驻留在内存中的、被称为启动加载器的操作系统代码,并创建一组新的代码、数据、堆和栈段。
- 新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start 地址,_start 最终调用 hello中的 main 函数。
- 除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到 CPU 引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
(以下格式自行编排,编辑时删除)
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
- 逻辑控制流:一系列程序计数器 PC 的值的序列叫做逻辑控制流,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程,进程轮流使用处理器,。
- 时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
- 用户模式和内核模式:处理器通常使用一个寄存器描述了进程当前享有的特权,对两种模式区分。设置模式位时,进程处于内核模式,该进程可以访问系统中的任何内存位置,可以执行指令集中的任何命令;当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。
- 上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态。它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
hello sleep进程调度的过程:
- 当调用 sleep 之前,如果 hello 程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换
- 上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行
- 保存以前进程的上下文
- 恢复新恢复进程被保存的上下文,
- 将控制传递给这个新恢复的进程 ,来完成上下文切换。
- hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将 hello 进程从运行队列中移出加入等待队列
- 定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时时(2.5secs)发送一个中断信号,
- 进入内核状态执行中断处理,将 hello 进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
(1)当程序完成之后,任意在键盘上按下一个数字,程序结束
- 在程序运行过程中通过ctrl z 暂停程序,此时,父进程收到SIGSTP的信号,暂停程序的运行,将子进程hello挂起。然后利用ps信号查看hello有没有被回收,得知它的作业号为1,之后通过fg 1继续打印剩余语句。
(3)在程序运行过程中通过ctrl c终止程序,父进程收到SIGINT信号,信号处理函数的逻辑是结束hello,并回收hello进程。
- 在程序运行过程中输入其他内容,此时将内容全部传到stdin中,hello结束后,stdin中的其他字串会当做shell若干条命令行输入。
pstree如下:
6.7本章小结
(以下格式自行编排,编辑时删除)
在本章中,我了解到了shell处理hello程序,shell通过fork创建新的子进程,在子进程中利用execve执行hello程序,并且了解了在hello执行过程中向终端输入一些东西会发生什么事。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
(以下格式自行编排,编辑时删除)
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
- 逻辑地址:程序代码经过编译后出现在 汇编程序中地址。逻辑地址由选择符(在实模式下是描述符,在保护模式下是用来选择描述符的选择符)和偏移量(偏移部分)组成。
- 线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。至于虚拟地址,只关注 CSAPP 课本中提到的虚拟地址,实际上就是这里的线性地址。
- 物理地址:CPU 通过地址总线的寻址,找到真实的物理内存对应地址。 CPU对内存的访问是通过连接着 CPU 和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
- 8086处理器的寄存器是16位的,之后引入了段寄存器,可以访问更多的地址空间但不改变寄存器和指令的位宽。8086共设计了20位宽的地址总线,逻辑地址为段寄存器左移4位加上偏移地址得到20位地址。
- 将内存分为不同的段,段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。分段功能在实模式和保护模式下有所不同:
- 实模式:逻辑地址=线性地址=实际的物理地址,即不设防。段寄存器存放真实段基址,同时给出32位地址偏移量,可以访问真实物理内存。
- 保护模式:线性地址还需要经过分页机制才能够得到物理地址,线性地址也需要逻辑地址通过段机制来得到。32位段基址被称作选择符,段寄存器无法放下,用于引用段描述符表中的表项来获得描述符。
7.3 Hello的线性地址到物理地址的变换-页式管理
通过分页机制实现线性地址(书里的虚拟地址VA)到物理地址(PA)之间的转换。分页机制是指对虚拟地址内存空间进行分页。
- 首先Linux系统有自己的虚拟内存系统,Linux将虚拟内存组织成一些段的集合,段之外的虚拟内存不存在因此不需要记录。
- 内核为hello进程维护一个段的任务结构即图中的task_struct,其中条目mmàmm_struct(描述了虚拟内存的当前状态),pgdà第一级页表的基地址(结合一个进程一串页表),mmapàvm_area_struct的链表。一个链表条目对应一个段,链表相连指出了hello进程虚拟内存中的所有段
- 虚拟页(VP):系统将每个段分割为大小固定的块,来作为进行数据传输的单元。对于linux,每个虚拟页大小为4KB。
- 物理页(PP/页帧):类似于虚拟页,物理内存也被分割。虚拟内存系统中MMU负责地址翻译,MMU使用页表,即存一种放在物理内存中的数据结构,将虚拟页到物理页映射,即虚拟地址到物理地址的映射。
- 通过页表基址寄存器 PTBR+VPN 在页表中获得条目 PTE,一条 PTE 中包含有效位、权限信息、物理页号。
- 如果有效位是 0+NULL 则代表没有在虚拟内存空间中分配该内存;
- 如果是有效位 0+非 NULL,则代表在虚拟内存空间中分配了但是没有被缓存到物理内存中;
- 如果有效位是 1 则代表该内存已经缓存在了物理内存中,可以得到其物理页号 PPN,与虚拟页偏移量共同构成物理地址 PA。
7.4 TLB与四级页表支持下的VA到PA的变换
在 Intel Core i7 环境下研究 VA 到 PA 的地址翻译问题。前提如下:
- 虚拟地址空间 48 位,物理地址空间 52 位,页表大小 4KB,4 级页表。
- TLB 4路 16 组相联。CR3 指向第一级页表的起始位置(上下文一部分)。
一个页表大小4KB,一个PTE条目8B,共 512(2e9)个条目,使用 9 位二进制索引,一共4个页表共使用36位二进制索à VPN 共 36 位。VPO = VA 48位-VPN36位= 12位; TLB共16组à。因为 TLBT = VPN 36位 - TLBI 4位=32位。
- CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配
- 如果命中,则得到 PPN(40bit)与 VPO(12bit)组合成 PA(52bit)。
- 如果 TLB 中没有命中,MMU 向页表中查询,CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,
- 以此类推,最终在第四级页表中查询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。
- 如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
7.5 三级Cache支持下的物理内存访问
前提:只讨论 L1 Cache 的寻址细节,L2 与 L3Cache 原理相同。L1 Cache 是 8路 64 组相联。块大小为 64B。
共64(2e6)组à需要 6bit CI 进行组寻址。共有8路,块大小为64Bà需要6bit CO表示数据偏移位置。因为 VA 共 52bit,所以CT 共 40bit。
- 根据上一步获得的物理地址 VA,使用CI(后六位再后六位)进行组索引,每组8路,对8路的块分别匹配 CT(前40位)。
- 如果匹配成功且块的 valid 标志位为1,则命中(hit),根据数据偏移量 CO(后六位)取出数据返回。
- 如果没有匹配成功或者匹配成功但是标志位是 1,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。
- 根据一种常见的简单策略,查询到数据之后,如果映射到的组内有空闲块,则直接放置;否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。
7.6 hello进程fork时的内存映射
当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序加载并运行 hello 需要以下几个步骤:
-
- 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
- 映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障是一种常见的故障,当指令引用一个虚拟地址,在 MMU 中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。
缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令再次发送 VA 到MMU,这次 MMU 就能正常翻译 VA 了
7.9动态存储分配管理
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
- 动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。
- 每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。
- 空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。
- 一个已分配的块保持已分配状态,直到它被释放。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
一、 带边界标签的隐式空闲链表
- 堆及堆中内存块的组织结构:
在内存块中增加4B的Header(用于寻找下一个blcok)和4B的Footer(用于寻找上一个block)。Footer的设计是专门为了合并空闲块方便的。因为Header和Footer大小已知,所以利用Header和Footer中存放的块大小就可以寻找上下 block。
- 隐式链表
对比于显式空闲链表,隐式空闲链表代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表。隐式空闲链表中Header和Footer中的block大小间接起到了前驱、后继指针的作用。
- 空闲块合并
可以利用Footer方便的对前面的空闲块进行合并。合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。只需要通过改变Header和Footer中的值,就可以完成对于四种情况分别进行空闲块合并。
二、 显示空间链表基本原理
将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个 pred(前驱)和 succ(后继)指针
7.10本章小结
本章主要介绍了hello的存储器地址空间、段式管理、hello 的页式管理, 以intel Core7在指定环境下介绍了VA 到PA 的变换、物理内存访问,还介绍hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
设备的模型化:所有的 IO 设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
8.2 简述Unix IO接口及其函数
- 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。内核返回一个小的非负整数,叫做描述符。描述符在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
- Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
- 改变当前的文件位置:内核保持着每个打开的文件的一个文件位置k。k初始为0。这个文件位置k表示的是从文件开头起始的字节偏移量。应用程序能够通过执行seek,显式地将改变当前文件位置 k,例如各种fread或fwrite。
- 读写文件:
- 读操作就是从文件复制n>0个字节到内存。从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的文件。当k>=m时,触发EOF。
- 写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k=k+n。
- 关闭文件:内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O 函数:
- int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的(即fopen的内层函数)。open函数将filename(文件名,含后缀)转换为一个文件描述符(C中表现为指针),并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件(读或写或两者兼具),mode参数指定了新文件的访问权限位(只读等)。
- int close(fd),fd是需要关闭的文件的描述符(C中表现为指针),close 返回操作结果。
- ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
- ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
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);
}
write函数:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello程序,从hello.c文件开始,经过cpp处理到达hello.i,hello.i再经过ccl处理变为hello.s,hello.s经过as处理后到达hello.o,hello.o再经过ld处理变为可执行文件hello,当我们在程序中输入./hello时,shell创造一个子进程,并且在子进程中执行hello,并输出。
在学习hello的过程中,我深刻的了解了程序是如何运行的过程,并且还顺带着了解了诸如高速缓存,主存方面的知识,了解到我们应当写方便计算机系统处理的程序,充分发挥局部性的优势。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i:cpp处理后的文件
hello.s:编译后的文件
hello.o:汇编后的可重定位目标执行
hello:可执行文件
hello.elf:hello.o的ELF格式
hello.objdump:hello.o反汇编代码
hello1.objdump:hello的反汇编代码
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
[7] 百度百科 预处理_百度百科
[8] 百度百科
[9] [转]printf 函数实现的深入剖析 - Pianistx - 博客园
(参考文献0分,缺失 -1分)