第1章 概述
1.1 Hello简介
1.1.1 P2P: From Program to Process
P2P,即从步伐到进程。用户利用高级语言C语言编写hello.c源步伐,经过cpp预处理形成hello.i,再经过ccl编译形成汇编语言步伐hello.s,然后经过as转换为呆板语言指令,形成可重定位目标步伐hello.o,末了通过ld与库函数链接并符号剖析与重定位,形成可执行目标文件hello。而后可执行文件hello通过shell加载,fork产生子进程,经过以上步调hello步伐(program)变成了hello进程(process)。
1.1.2 020: From Zero-0 to Zero -0
020,即从运行到结束。初始时内存中没有hello文件相干的内容,通过fork产生hello子进程后,通过execve举行加载,先删除当前假造所在已存在的数据布局,为hello的代码、数据、bss等创建地区,然后映射共享地区,设置步伐计数器,进入main函数,CPU分配时间片执行逻辑控制流。执行过程中,假造内存为进程提供独立的空间;存储布局层层递进,让数据从磁盘传输到CPU中;TLB、分级页表等也为数据的高效访问提供保障;I/O装备通过描述符与接口实现了hello的输入输出。多方面合作配合之下,hello完成执行。然后,shell回收hello进程,删除hello的所有陈迹,开释运行中占用的内存空间。至此,hello从运行到结束,完成020过程。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk
1.2.2 软件环境
Windows 10 64位;Vmware-workstation-16;Ubuntu 20.04 LTS 64位
1.2.3 开发与调试工具
Codeblocks 64位;vi/vim/gedit+gcc
1.3 中心结果
表1.3-1 中心结果
文件名
| 功能
| hello.c
| 源代码
| hello.i
| 预处理后的文本文件
| hello.s
| 编译后的汇编文件
| hello.o
| 汇编后的可重定位目标文件
| hello
| 链接后的可执行文件
| hello.elf
| 用readelf读取hello.o的ELF格式信息
| hello.asm
| 反汇编hello.o的反汇编文件
| hello2.elf
| 由hello可执行文件天生的.elf文件
| hello2.asm
| 反汇编hello可执行文件得到的反汇编文件
|
1.4 本章小结
本章重要介绍了hello的P2P和020过程,从总体上扼要论述了hello的一生,给出了论文研究时的环境与工具以及中心天生的文件信息。
第2章 预处理
2.1 预处理的概念与作用
2.1.1概念
预处理器(cpp) 根据以字符#开头的命令,修改原始的C 步伐,形成完整的文件。C语言的预处理重要有三个方面的内容: 1.宏定义;(#define 标识符 文本) 2.文件包含;(#include "文件名") 3.条件编译。(#ifdef,#else,#endif)
2.1.2作用
- 宏替换。例如:#define a 10,则在预处理时将所有的a变为10。
- 文件包含。预处理器读取头文件中的内容,并插入到步伐文本中。例如:#include <stdio.h>则将stdio.h的代码扎入到hello中。
- 删除注释。
条件编译。根据某个条件判定举行静态编译。重要有#ifdef, #else, #elif, #endif, #ifndef等条件语句。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
图2-1.预处理命令
2.3 Hello的预处理结果剖析
预处理后的源文件从23行扩展到了3060行,main函数在第3047行,预处理器将头文件中的内容引入hello.i,将需要用到的库函数等参加到了文本中,让步伐能够继续被编译器编译。
图2.3-1 hello.i中部分文件信息
图2.3-2 hello.i中部分类型定义信息
图2.3-3 hello.i中部分函数声明信息
图2.3-4 hello.i中源码部分
2.4 本章小结
在本章中,我们对hello.c举行了预处理,天生hello.i,预处理器会举行宏展开、头文件展开、条件编译等处理,并删除注释,对函数源码并不做过多修改,hello.i文件可用于下一步的处理。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译是指预处理后,编译器(ccl)将预处理文件hello.i翻译成汇编语言文件hello.s。
3.1.2 编译的作用
- 语法查抄:查抄代码是否存在语法错误,假如有错误的话就会报错。
- 天生汇编代码:将步伐翻译成汇编语言,从而在下一阶段可以让汇编器翻译成呆板语言指令。
- 代码优化:编译器会对步伐举行优化,天生效率更高的目标代码。
3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s
图3.2-1 编译命令
3.3 Hello的编译结果剖析
3.3.1 汇编代码展示
- .file "hello.c"
- .text
- .section .rodata
- .align 8
- .LC0:
- .string "\323\303\267\250: Hello 2022112322 \300\356\352\277\342\371 15645376975 0! "
- .LC1:
- .string "Hello %s %s %s\n"
- .text
- .globl main
- .type main, @function
- 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 $5, -20(%rbp)
- je .L2
- leaq .LC0(%rip), %rdi
- call puts@PLT
- movl $1, %edi
- call exit@PLT
- .L2:
- movl $0, -4(%rbp)
- jmp .L3
- .L4:
- movq -32(%rbp), %rax
- addq $24, %rax
- movq (%rax), %rcx
- movq -32(%rbp), %rax
- addq $16, %rax
- movq (%rax), %rdx
- movq -32(%rbp), %rax
- addq $8, %rax
- movq (%rax), %rax
- movq %rax, %rsi
- leaq .LC1(%rip), %rdi
- movl $0, %eax
- call printf@PLT
- movq -32(%rbp), %rax
- addq $32, %rax
- movq (%rax), %rax
- movq %rax, %rdi
- call atoi@PLT
- movl %eax, %edi
- call sleep@PLT
- addl $1, -4(%rbp)
- .L3:
- cmpl $9, -4(%rbp)
- jle .L4
- call getchar@PLT
- movl $0, %eax
- leave
- .cfi_def_cfa 7, 8
- ret
- .cfi_endproc
- .LFE6:
- .size main, .-main
- .ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0"
- .section .note.GNU-stack,"",@progbits
- .section .note.gnu.property,"a"
- .align 8
- .long 1f - 0f
- .long 4f - 1f
- .long 5
- 0:
- .string "GNU"
- 1:
- .align 8
- .long 0xc0000002
- .long 3f - 2f
- 2:
- .long 0x3
- 3:
- .align 8
- 4:
| 3.3.2 汇编文本布局分析
表3.2.2-1 hello.s文件布局
内容
| 含义
| .file
| 源文件
| .text
| 代码段
| .global
| 全局变量
| .data
| 存放已经初始化的全局和静态C 变量
| .section .rodata
| 存放只读变量
| .align
| 对齐方式
| .type
| 表示是函数类型/对象类型
| .size
| 表示巨细
| .long .string
| 表示是long类型/string类型
| 3.3.3 常量
编译时对常量举行编码,并将其存储在只读代码区的 .rodata节,在步伐运行时会直接通过寻址找到常量。
例如将hello.c中“Usage: Hello 学号 姓名 手机号 秒数!”编译为汇编代码第6行:
- .string "\323\303\267\250: Hello 2022112322 \300\356\352\277\342\371 15645376975 0! "
3.3.4 变量
差别类型的变量在差别位置定义,初始化的全局变量和静态变量定义在只读代码区的.bss节,已初始化的全局和静态变量定义在只读代码区的.data节,局部变量在堆上举行定义和开释。
例如,局部变量int i被保存在栈上,通过呆板指令对其赋值
3.3.5 赋值操纵
对局部变量举行赋值操纵,使用MOV指令,根据差别的数据巨细选择差别指令movb、movw、movl、movq等。具体见3.3.4例。
3.3.5 算术运算
表3.3.5-1 算数指令
指令
| 效果
| leaq s,d
| d=&s
| inc d
| d+=1
| dec d
| d-=1
| neg d
| d=-d
| add s,d
| d=d+s
| sub s,d
| d=d-s
| imulq s
| r[%rdx]:r[%rax]=s*r[%rax]
| mulq s
| r[%rdx]:r[%rax]=s*r[%rax]
| idivq s
| r[%rdx]=r[%rdx]:r[%rax] mod s
r[%rax]=r[%rdx]:r[%rax] div s
| divq s
| r[%rdx]=r[%rdx]:r[%rax] mod s
r[%rax]=r[%rdx]:r[%rax] div s
| 在hello.s中,例如,实现i++的操纵:
开发栈以及回收栈:
3.3.6 比较和跳转操纵
通过COM指令举行比较,计算两个值相减巨细,根据结果设置条件码,根据条件码来判定跳转值,也可通过跳转指令J判定有无符号。
例如,查抄argc是否未便是4。在hello.s中,使用cmpl $4,-20(%rbp),比较 argc与4的巨细并设置条件码,为下一步je利用条件码举行跳转作准备。
- cmpl $5, -20(%rbp)
- je .L2
表3.3.6-1 跳转指令
指令
| 条件
| jmp
| 直接跳转
| je
| 相等
| Jne
| 未便是
| Js
| 小于
| Jns
| 小于便是
| Jg
| 大于
| Jge
| 大于便是
| Ja
| 大于(无符号)
| Jae
| 大于便是(无符号)
| jbe
| 小于便是(无符号)
|
3.3.7 数组/指针操纵
对数组的索引相当于在第一个元素所在的底子上通过加索引值乘以数据巨细来实现。
例如,在hello.c中,存在char *argv[],根据图3-3可知,根据argv首所在获得argv[1]和argv[2]需要通过加减操纵:
- movq -32(%rbp), %rax
- addq $16, %rax
- movq (%rax), %rdx
- movq -32(%rbp), %rax
- addq $8, %rax
- movq (%rax), %rax
- movq %rax, %rsi
3.3.8 函数操纵
hello.c中包括main函数,printf函数,sleep函数,getchar函数,exit函数。
起首,内核shell获取命令行参数和环境变量所在,执行main函数,在main中需要调用其它函数,在main中为被调用函数分配栈空间。调用函数需要借助栈,先将返回所在压入栈中,并将PC设为被调用函数的起始所在,然后调用。返回时,先从栈中弹出返回所在,再PC设置为该返回所在。return正常返回后,leave规复栈空间。
在hello.s中调用函数有:
call puts@PLT
call exit@PLT
call printf@PLT
call sleep@PLT
call getchar@PLT
3.4 本章小结
本章重要探讨编译器将经过预处理阶段后的C步伐hello.i翻译成汇编语言步伐的处理过程,包括对数据、算术操纵、关系操纵、控制转移、数组操纵、函数操纵的处理。编译器也会在处理过程中对步伐举行一些优化,最终的结果被保存在hello.s文件中,能够在下一阶段让汇编器翻译呆板语言指令。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是指汇编器(as)将hello.s翻译成呆板语言指令的过程,把这些指令打包成可重定位目标步伐的格式,并将结果保存在目标文件hello.o中。
4.1.2 汇编的作用
将汇编指令转换成呆板可以直接读取分析的呆板指令,天生hello.o文件,用于后续的链接。
4.2 在Ubuntu下汇编的命令
汇编命令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
图4.2-1 汇编命令
4.3 可重定位目标elf格式
在shell中输入readelf -a hello.o > hello.elf指令获得hello.o文件的 ELF 格式:
图4.3-1 天生elf格式文件
4.3.1 ELF头
以 16字节序列 Magic 开始,其描述了天生该文件的体系的字的巨细和字节顺序,ELF 头剩下的部分包含资助链接器语法分析和解释目标文件的信息,此中包括 ELF 头巨细、目标文件类型、呆板类型、节头部表的文件偏移,以及节头部表中条目的巨细和数目等相干信息。
图4.3.1-1 ELF头
4.3.2 节头
节头记录了各节名称及巨细、类型及全体巨细、所在及旗标、毗连、信息和偏移量及对齐信息。
图4.3.2-1节头
4.3.3 重定位节
当链接器把这个目标文件和其他文件组适时,需要修改表中的这些位置。一样平常,调用外部函数或者引用全局变量的指令都需要修改。
此中包括R_X86_64_PC32( PC相对所在的引用)和R_X86_64_32(绝对所在的引用)。
图4.3.3-1重定位节
4.3.4 符号表
符号表中保存着定位、重定位步伐中符号定义和引用的信息,所有重定位需要引用的符号都在此中声明。
图4.3.4-1符号表
4.4 Hello.o的结果剖析
使用objdump -d -r hello.o > hello.asm分析hello.o的反汇编,并与第3章的 hello.s文件举行对照分析。
图4.4-1天生反汇编文件
4.4.1 反汇编代码
图4.4-2反汇编代码
4.4.2 与汇编代码比较
将hello.asm和hello.s举行比较,大部分相同,重要有一下几个方面差别:
hello.s中包含.type .size .align以及.rodata只读数据段等信息,而hello.asm中只有函数的相干内容。
2.分支转移:
在hello.s中,跳转指令的目标所在直接记为段名称,如.L1,.L2等。而在反汇编得到的hello.asm中,跳转的目标为具体的所在。
3.函数调用:
在hello.s文件中,call之后直接跟着函数名称,而在反汇编得到的hello.asm中,call 的目标所在是当前指令的下一条指令。
4. 全局变量访问:
在hello.s 文件中,使用段名称+%rip访问 rodata(printf 中的字符串),而在反汇编得到的hello.asm中,使用 0+%rip举行访问。
4.4.3 呆板语言的构成
呆板语言是呆板能直接识别的步伐语言或指令代码,无需经过翻译,每一操纵码在计算机内部都有相应的电路来完成它,或指不经翻译即可为呆板直接理解和继续的步伐语言或指令代码。呆板语言使用绝对所在和绝对操纵码。差别的计算机都有各自的呆板语言,即指令体系。从使用的角度看,呆板语言是最低级的语言。
4.4.4 呆板语言与汇编语言的映射关系
汇编语言和呆板语言一样平常是一一对应的,汇编语言是呆板语言的符号表示方式。而差别类型的CPU 有差别的呆板指令体系,也就有差别的汇编语言,所以,汇编语言步伐与呆板有着密切的关系。所以,除了同系列、差别型号CPU 之间的汇编语言步伐有肯定程度的可移植性之外,其它差别类型(如:小型机和微机等)CPU 之间的汇编语言步伐是无法移植的,也就是说,汇编语言步伐的通用性和可移植性要比高级语言步伐低。
4.5 本章小结
本章讨论了汇编阶段将汇编代码hello.s翻译成呆板语言指令hello,o的过程,等待下一步链接器的处理。同时,比较了汇编代码与反汇编代码之间的差别之处。
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是指链接器(ld)将各种代码和数据片断收集并组合成一个单一可执行目标文件的过程。
5.1.2 链接的作用
使得分离编译成为可能,我们可以独立的修改和编译模块,当我们改变这些模块的此中一个时,只需简单的重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
链接命令:ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o hello.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o -o hello
图5.2-1 链接命令
5.3 可执行目标文件hello的格式
在Shell中输入命令 readelf -a hello > hello2.elf 天生 hello 步伐的 ELF 格式文件,保存为hello2.elf:
图5.3-1 天生hello.o的elf格式
5.3.1 ELF头
以 16字节序列 Magic 开始,其描述了天生该文件的体系的字的巨细和字节顺序,ELF 头剩下的部分包含资助链接器语法分析和解释目标文件的信息,此中包括 ELF 头巨细、目标文件类型、呆板类型、节头部表的文件偏移,以及节头部表中条目的巨细和数目等相干信息。
图5.3.1-1 ELF头
5.3.2 节头
节头记录了各节名称及巨细、类型及全体巨细、所在及旗标、毗连、信息和偏移量及对齐信息。
图5.3.2-1节头
5.3.3 步伐头
一个布局数组,描述了体系准备步伐执行所需的段或其他信息。
图5.3.3-1步伐头
5.3.4 Dynamic section(动态section)
图5.3.4-1 Dynamic section
5.3.5 符号表
符号表中保存着定位、重定位步伐中符号定义和引用的信息,所有重定位需要引用的符号都在此中声明。
图5.3.5-1符号表
5.4 hello的假造所在空间
使用edb加载hello,查看本进程的假造所在空间各段信息,并与5.3对照分析说明。
根据计算机体系的特性,步伐被载入至所在0x400000~0x401000中。在该所在范围内,每个节的所在都与前一节中节对应的 Address 相同。通过ELF可知,步伐从0x00400000到0x00400fff,在0x400fff之后存放的是.dynamic到.shstrtab节的内容。
图5.4-1用edb查看假造所在空间
5.5 链接的重定位过程分析
在Shell中使用命令objdump -d -r hello > hello2.asm天生反汇编文件hello2.asm,与第四章中天生的hello.o.asm文件举行比较:
图5.5-1天生反汇编文件
5.5.1 重定位概念
链接器在完成符号剖析以后,就把代码中的每个符号引用和一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节简直切巨细。
然后就可以开始重定位步调了,在这个步调中,将归并输入模块,并为每个符号分配运行时的所在。在 hello 到 hello.o 中,起首是重定位节和符号定义,链接器将所有输入到 hello 中相同类型的节归并为同一类型的新的聚合节。当这一步完成时,步伐中的每条指令和全局变量都有唯一的运行时内存所在了。然后是重定位节中的符号引用,链接器会修改 hello 中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行所在。
5.5.2 hello与hello.o的差别
1.链接后函数数目增长,hello中增长了外部函数。
2.函数调用指令call的参数发生变化,hello中调用为call+函数名,hello.o中为call+相对偏移所在。
3.跳转指令参数发生变化。
4.hello.o中是相对偏移所在,hello为假造内存所在。
5.6 hello的执行流程
hello在执行的过程中要执行载入、执行和退出三个过程,列出其调用与跳转的各个子步伐名或步伐所在。
表5.6-1 步伐名称与步伐所在
步伐名称
| 步伐所在
| <_start>
| 4010f0
| <__libc_csu_init>
| 4011c0
| <_init>
| 401000
| <main>
| 401125
| <.plt>
| 401020
| <puts@plt>
| 401090
| <printf@plt>
| 4010a0
| <atoi@plt>
| 4010c0
| <exit@plt>
| 4010d0
| <sleep@plt>
| 4010e0
| <getchar@plt>
| 4010b0
| <__libc_csu_fini>
| 401230
| <_fini>
| 401238
|
5.7 Hello的动态链接分析
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,此中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行步伐调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,此中每个条目是8字节所在。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在剖析函数所在时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其所在需要在运行时被剖析。每个条目都有一个相匹配的PLT条目。
图5.7-1 全局偏移表的位置
图5.7-2 调用 dl_init 之前的全局偏移表
图5.7-3 调用 dl_init 之后的全局偏移表
由上图对比可知,调用dl_init函数后,发生动态链接,GOT条目改变。
5.8 本章小结
本章介绍了链接的概念和功能,分析可执行文件hello的ELF格式及其假造所在空间,并对重定位、动态链接举行深入的分析。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程的经典定义就是一个执行中的步伐的实例。进程是计算机中的步伐关于某数据聚集上的一次运行运动,是体系举行资源分配和调度的根本单元,是操纵体系布局的底子。在早期面向进程设计的计算机布局中,进程是步伐的根本执行实体;在当代面向线程设计的计算机布局中,进程是线程的容器。步伐是指令、数据及其构造形式的描述,进程是步伐的实体。
6.1.2 进程的作用
每次用户通过向 shell 输入一个可执行目标文件的名字,运行步伐时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用步伐也能够创建新进程,而且在这个新进程的上下文中运行它们自己的代码或其他应用步伐。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell作用
读取输入并剖析命令行;替换特别字符,比如通配符和汗青命令符;设置管道、重定向和后台处理;处理信号;程式执行的相干设置。
1. 可交互,和非交互的使用shell。在交互式模式,shell从键盘吸收输入;在非交互式模式,shell从文件中获取输入。
2. shell中可以同步和异步的执行命令。在同步模式,shell要等命令执行完,才能吸收下面的输入。在异步模式,命令运行的同时,shell就可吸收其它的输入。重定向功能,可以更细致的控制命令的输入输出。另外,shell允许设置命令的运行环境。
3. shell提供了少量的内置命令,以便自身功能更加完备和高效。
4. shell除了执行命令,还提供了变量,流程控制,引用和函数等,类似高级语言一样,能编写功能丰富的步伐。
5. shell强大的的交互性除了可编程,还体现在作业控制,命令行编辑,汗青命令,和别名等方面。
6.2.2 Shell 处理流程
命令行是一串 ASCII 字符由空格分隔。字符串的第一个单词是一个可执行步伐,或者是 shell 的内置命令。命令行的其余部分是命令的参数。
假如第一个单词是内置命令,shell 会立刻在当前进程中执行。否则,shell 会新建一个子进程,然后再子进程中执行步伐。新建的子进程又叫造作业。通常,作业可以由 Unix 管道毗连的多个子进程构成。
假如命令行以&符号末端,那么作业将在后台运行,这意味着在打印提示符并等待下一个命令之前,shell 不会等待作业终止。否则,作业在前台运行,这意味着 shell 在作业终止前不会执行下一条命令行。 因此,在任何时候,最多可以在一个作业中运行在前台。 但是,恣意数目的作业可以在后台运行。例如,键入命令行:sh> jobs,会让 shell 运行内置命令 jobs。键入命令行 sh> /bin/ls -l -d 会导致 shell 在前台运行 ls 步伐。根据约定,shell会执行步伐的 main 函数 int main(int argc, char *argv[]),argc 和 argv 会吸收到下面的值:
argc == 3,
argv[0] == ‘‘/bin/ls’’,
argv[1]== ‘‘-l’’,
argv[2]== ‘‘-d’’.
下面以&末端的命令行会在后台执行 ls 步伐:
sh> /bin/ls -l -d &
Unix shell 支持作业控制的概念,允许用户在前台和后台之间往返移动作业,并更改进程的状态(运行,停止或终止)。在作业运行时,键入 ctrl-c会将 SIGINT 信号通报到前台作业中的每个进程。SIGINT 的默认动作是终止进程。类似地,键入 ctrl-z 会导致 SIGTSTP 信号通报给所有前台进程。SIGTSTP 的默认操纵是停止进程,直到它被 SIGCONT 信号叫醒为止。Unixshell 还提供支持作业控制的各种内置命令。例如:
jobs:列出运行和停止的后台作业。
bg <job>:将停止的后台作业更改为正在运行的后台作业。
fg <job>:将停止或运行的后台作业更改为在前台运行。
kill <job>:终止作业。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的、处于运行状态的子进程。
函数原型:int fork(void);
调用fork函数后,子进程返回0,父进程返回子进程的PID;新创建的子进程险些但不完全与父进程相同:子进程得到与父进程假造所在空间相同的(但是独立的)一份副本;子进程获得与父进程任何打开文件描述符相同的副本;但是子进程有差别于父进程的PID。fork函数:被调用一次,却返回两次!
Fork具体处理hello文件过程:
起首,带参执行当前目次下的可执行文件hello,父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包括栈、通用寄存器、步伐计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程假如仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。
图6.3-1步伐执行过程
6.4 Hello的execve过程
调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新步伐hello。execve 函数从不返回,它将删除该进程的代码和所在空间内的内容并将其初始化,然后通过跳转到步伐的第一条指令或入口点来运行该步伐。将私有的地区映射进来,例如打开的文件,代码、数据段,然后将公共的地区映射进来。后面加载器跳转到步伐的入口点,即设置PC指向_start 所在。_start函数最终调用hello中的 main 函数,这样,便完成了在子进程中的加载。
6.5 Hello的进程执行
6.5.1 进程时间片
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
6.5.2 上下文信息
上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、步伐计数器、用户栈、状态寄存器、内核栈和各种内核数据布局等对象的值构成。
6.5.3 用户模式和内核模式
处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用所在空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,而且可以访问体系中的任何内存位置。
6.5.4 执行过程
进程调度指在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。内存收到停止信号之后,将当前进程参加等待序列,举行上下文切换将当前的进程控制权交给其他进程,当再次收到停止信号时将hello从等待队列参加运行队列。
6.6 hello的非常与信号处理
非常可以分为四类:停止(interrupt),陷阱(trap),故障(fault)和终止(abort)。
停止是异步发生的,是来自处理器外部的I/O装备的信号的结果。在当前指令完成执行后,处理器留意到停止引脚的电压变高,就从体系总线读取非常号,然后调用适当的停止处理步伐。当处理步伐返回后,它就将控制返回给下一条指令。
陷阱是有意的非常,是执行一条指令的结果。应用步伐执行一次体系调用,然后把控制通报给处理步伐,陷阱处理步伐运行后,返回到syscall之后的指令。
故障由错误环境引起,故障发生时处理器将控制转移给故障处理步伐。假如处理步伐能够修正这个错误环境,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理步伐返回到内核中的abort例程,abort例程会终止引起故障的应用步伐。
终止是不可规复的致命错误造成的结果。终止处理步伐从不将控制返回给应用步伐,处理步伐将控制返回给一个abort例程,该例程会终止这个应用步伐。
以下给出步伐运行过程中按键盘,如不绝乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令的运行结果:
图6.6-1 执行Ctrl-c
图6.6-2 执行Ctrl-z以及ps、jobs
图6.6-4 执行pstree
图6.6-5 执行kill
图6.6-6 执行fg返回前台
6.7本章小结
本章重要讨论了进程和shell的概念与作用,进程的创建和执行过程,以及对非常和信号的处理。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器所在空间
逻辑所在:逻辑所在是指由步伐产生的与段相干的偏移所在部分,即hello.o 里面的相对偏移所在。
线性所在:所在空间是一个非负整数所在的有序聚集,假如所在空间中的整数是连续的,那么我们说它是一个线性所在空间,即hello里面的假造内存所在。
假造所在:CPU 通过天生一个假造所在,即hello里面的假造内存所在。
物理所在:计算机体系的主存被构造成一个由M 个连续的字节巨细的单元构成的数组。每字节都有一个唯一的物理所在。就是hello在运行时假造内存所在对应的物理所在。
7.2 Intel逻辑所在到线性所在的变换-段式管理
一个逻辑所在由两部分构成,段标识符和段内偏移量。段标识符由 16 位字段构成,前 13 位为索引号。索引号是段描述符的索引,很多个描述符,构成了一个数组,叫做段描述表,可以通过段描述标识符的前 13 位,在这个表中找到一个具体的段描述符,这个描述符就描述了一个段,每个段描述符由八个字节构成。base 字段,描述了段开始的线性所在,一些全局的段描述符,放在全局段描述符表中,一些局部的则对应放在局部段描述符表中。由 T1 字段决定使用哪个。
图7-1.段选择符
以下是具体的转化步调:
1. 给定一个完整的逻辑所在,[段选择符:段内偏移所在]
2. 看段选择符 T1,知道要转换的是 GDT 中的段还是 LDT 中的段,通过寄存器得到所在和巨细。
3. 取段选择符中的 13 位,再数组中查找对应的段描述符,得到 BASE,就是基所在。
4. 线性所在便是基所在加所在偏移量。
对于偏移量,基址寄存器还是变址寄存器有差别的计算方法,后者需要经过乘比例因子等处理。
7.3 Hello的线性所在到物理所在的变换-页式管理
ntel处理器从线性所在到物理所在的变换通过页式管理实现。
图7-2.二级页表布局
具体步调为:
1、从CR3中取出进程的页目次的所在,取出其前20位,这是页目次的基所在;
2、根据取出来的页目次的基所在以及线性所在的前十位,举行组合得到线性所在的前十位的索引对应的项在页目次中所在,根据该所在可以取到该所在上的值,该值就是二级页表项的基址;
3、根据第二步取到的页表项的基址,取其前20位,将线性所在的10-19位左移2位,按照和第2步相同的方式举行组合就可以得到线性所在对应的物理页框在内存中的所在在二级页表中的所在的起始所在,根据该所在向后读四个字节就得到了线性所在对应的物理页框在内存中的所在在二级页表中的所在,然后取该所在上的值就得到线性所在对应的物理页框在内存中的基所在;
4、根据第3步取到的基所在,取其前20位得到物理页框在内存中的基址,再根据线性所在末了的12位的偏移量得到具体的物理所在,取该所在上的值就是末了要得到值;
7.4 TLB与四级页表支持下的VA到PA的变换
针对Intel Core i7 CPU研究VA到PA的变换。
Intel Core i7 CPU的根本参数如下:
- 假造所在空间48位(n=48)
- 物理所在空间52位(m=52)
- TLB四路十六组相连
- L1,L2,L3块巨细为64字节
- L1,L2八路组相连
- L3十六路组相连
- 页表巨细4KB(P=4x1024=2^12),四级页表,页表条目(PTE)巨细8字节
由上述信息可以得知,VPO与PPO有p=12位,故VPN为36位,PPN为40位。单个页表巨细4KB,PTE巨细8字节,则单个页表有512个页表条目,需要9位二进制举行索引,而四级页表则需要36位二进制举行索引,对应着36位的VPN。TLB有16组,故TLBI有t=4位,TLBT有36-4=32位。
图 50 TLB与四级页表支持下的VA到PA的变换
如图所示, CPU产生假造所在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中添加条目。多级页表的工作原理展示如下:
s
若查询PTE的时候发现不在物理内存中,则引发缺页故障。假如发现权限不敷,则引发段错误。
7.5 三级Cache支持下的物理内存访问
图7-5. 三级Cache支持下的物理内存访问
CPU 发出一个假造所在在 TLB 里搜刮,假如掷中,直接发送到 L1cache 里;假如没有掷中,就现在加载到表里之后再发送已往,到了 L1 中,探求物理所在又要检测是否掷中,假如没有掷中,就向 L2/L3 中查找。这就用到了 CPU高速缓存。
在层与层之间读取数据时,起首在高速缓存中查找所需字w的副本。假如掷中,立刻返回字w给CPU。假如不掷中,从存储器条理布局中较低条理中取出包含字w的块,将这个块存储到某个高速缓存行中(可能会驱逐一个有效的行),然后返回字w。
读取数据分为三种:
1)直接映射高速缓存
直接映射高速缓存每个组只有一行,当CPU执行一条读内存字w的指令,它会向L1高速缓存请求这个字。假如L1高速缓存中有w的一个缓存副本,那么就会得到L1高速缓存掷中,高速缓存会很快抽取出w,并将它返回给CPU。否则就是缓存不掷中,当L1高速缓存向主存请求包含w的块的一个副本时,CPU必须等待。当被请求块最终从内存到达时,L1高速缓存将这个块存放在它的一个高速缓存行里,从被存储的块中抽取出字w,然后将它返回给CPU。确定是否掷中然后抽取的过程分为三步:(1)组选择;(2)行匹配;(3)字选择。
组选择即从w的所在中心抽取出s个索引位,将其解释为一个对应组号的无符号整数,从而找到对应的组;行匹配即对组内的唯一一行举行判定,当有效位为1且标志位与从所在中抽取出的标志位相同则成功匹配,否则就得到不掷中;而字选择即在行匹配的底子上通过所在的后几位得到块偏移,从而在高速缓存块中索引到数据。
图7-6.组选择
图7-7.行匹配和字选择
2)组相联高速缓存
组相联高速缓存每个组内可以多于一个缓存行,总体逻辑类似于直接映射高速缓存,差别之处在于行匹配时每组有更多的行可以实行匹配,遍历每一行。假如不掷中,有空行时也就是冷不掷中则直接存储在空行;假如没有空行也就是冲突不掷中,则替换已有行,通常有LFU(最不常使用)、LRU(最近最少使用)两者替换计谋。
3)全相联高速缓存
全相联高速缓存只有一个组,且这个组包含所有的高速缓存行(即E =C/B)。对于全相联高速缓存,因为只有一个组,组选择变的十分简单。所在中不存在索引位,所在只被分别为一个标志位和一个块偏移。行匹配和字选择同组相联高速缓存。
写入数据时,有两种方法来更新w在条理布局中紧接着低一层中的副本,分别是直写和写回:
1)直写
立刻将w的高速缓存块写回到紧挨着的低一层中。虽然简单,但是每次写都会引起总线流量。其处理不掷中的方法是非写分配,即避开高速缓存,直接将这个字写到低一层去。
(2)写回
尽可能地推迟更新,只有当替换算法要驱逐这个更新过的块时,才把它写到紧接着的低一层中。虽然能明显地减少总线流量,但是增长了复杂性,必须为每个高速缓存行增长一个额外的修改位,表明是否被修改过。写回处理不掷中的方法是写分配,加载相应低一层中的块到高速缓存中,然后更新这个高速缓存块,试图利用写的空间局部性,但会导致每次不掷中都会有一个块从低一层传到高速缓存。
通过这样的Cache读写机制,实现了从CPU寄存器到L1高速缓存,再到L2高速缓存,再到L3高速缓存,再到物理内存的访问,极大地提高了速度和效率。
7.6 hello进程fork时的内存映射
图7-8.fork进程
当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据布局,并分配给它一个唯一的PID。为了给这个新的hello创建假造内存,它创建了当前进程的mm_struct、地区布局和页表的原样副本。它将两个进程中的每个页面都标志为只读,并将两个进程中的每个地区布局都标志为私有的写时复制。
当fork在新进程中返回时,新进程现在的假造内存刚好和调用fork时存在的假造内存相同。当着两个进程中的任一个后来举行写操纵时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有所在空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含可执行目标文件hello中的步伐,加载、运行 hello 需要以下步调:
1. 删除已存在的用户地区。删除 shell 假造所在的用户部分中的已存在的地区布局。
2. 映射私有地区。为 hello 的代码、数据、bss 和栈地区创建新的地区布局。所有这些新的地区都是私有的、写时复制的。代码和数据地区被映射为hello 文件中的.text 和.data 区。bss 地区是请求二进制零的,映射到匿名文件,其巨细包含在 hello 中。栈和堆地区也是请求二进制零的,初始长度为零。
3. 映射共享地区。假如 hello 步伐与共享对象(或目标)链接,比如尺度 C 库libc. so, 那么这些对象都是动态链接到这个步伐的,然后再映射到用户假造所在空间中的共享地区内。
4. 设置步伐计数器(PC) 。execve 做的末了一件事变就是设置当前进程上下文中的步伐计数器,使之指向代码地区的入口点。
经过这个内存映射的过程,在下一次调度hello进程时,就能够从hello的入口点开始执行了。
7.8 缺页故障与缺页停止处理
图7-9.缺页
DRAM 缓存不掷中称为缺页(page fault). 图7-9为缺页之前我们的示例页表的状态。CPU 引用了 VP 3 中的一个字,VP 3 并未缓存在 DRAM 中。所在翻译硬件从内存中读取 PTE 3, 从有效位推断出 VP 3 未被缓存,而且触发一个缺页非常。缺页非常调用内核中的缺页非常处理步伐,该步伐会选择一个牺牲页,在此例中就是存放在 PP3 中的 VP4 。假如 VP4 已经被修改了,那么内核就会将它复制回磁盘。无论哪种环境,内核都会修改 VP4 的页表条目,反映出 VP 4 不再缓存在主存中这一事实。
7.9动态存储分配管理
动态内存分配器维护着一个进程的假造内存地区,称为堆(heap) 。体系之间细节差别,但是不失通用性,假设堆是一个请求二进制零的地区,它紧接在未初始化的数据地区后开始,并向上生长(向更高的所在) 。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。
分配器将堆视为一组差别巨细的块(block) 的聚集来维护。每个块就是一个连续的假造内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保存为供应用步伐使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被开释,这种开释要么是应用步伐显式执行的,要么是内存分配器自身隐式执行的。
根本方法:这里指的根本方法应该是在归并块的时候使用到的方法,有最佳适配和第二次适配还有首次适配方法,首次适配就是指的是第一次遇到的就直接适配分配,第二次顾名思义就是第二次适配上的,最佳适配就是搜刮完以后最佳的方案,固然这种的会在搜刮速度上大有低落。
计谋:这里的计谋指的就是显式的链表的方式分配还是隐式的标签引脚的方式分配还是分离适配,带边界标签的隐式空闲链表分配器允许在常数时间内举行对前面块的归并。这种思想是在每个块的末端处添加一个脚部,此中脚部就是头部的一个副本。假如每个块包括这样一个脚部,那么分配器就可以通过查抄它的脚部,判定前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。显式空间链表就是将空闲块构造为某种形式的显式数据布局。因为根据定义,步伐不需要一个空闲块的主体,所以实现这个数据布局的指针可以存放在这些空闲块的主体里面。例如,堆可以构造成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针,使首次适配的分配时间从块总数的线性时间减少到了空闲块数目的线性时间。为了分配一个块,必须确定请求的巨细类,而且对适当的空闲链表做首次适配,查找一个合适的块。假如找到了一个,那么就(可选地)分割它,并将剩余的部分插入到适当的空闲链表中。假如找不到合适的块,那么就搜刮下一个更大的巨细类的空闲链表。云云重复,直到找到一个合适的块。假如空闲链表中没有合适的块,那么就向操纵体系请求额外的堆内存,从这个新的堆内存中分配出一个块,将剩余部分放置在适当的巨细类中。要开释一个块,我们执行归并,并将结果放置到相应的空闲链表中。
图7-10.使用边界标志的堆块
7.10本章小结
本章结合书上第六章和第九章的知识(重要是第九章),介绍了存储器的所在空间,讲述了假造所在、物理所在、线性所在、逻辑所在的概念,还有进程fork和execve时的内存映射,并具体描述了体系如何应对缺页非常,末了描述了malloc的内存分配管理机制。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO装备管理方法
装备的模子化:文件
装备管理:unix io接口
所有的IO装备都被模子化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将装备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种同一且一致的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。
8.2 简述Unix IO接口及其函数
一个应用步伐通过要求内核打开相应的文件,来宣告它想要访问一个I/O装备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操纵中标识这个文件,内核记录有关这个打开文件的所有信息。对于Shell创建的每个进程,其都有三个打开的文件:尺度输入,尺度输出,尺度错误。
对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用步伐能够通过执行seek,显式地将改变当前文件位置k。
一个读操纵就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增长到k+n,给定一个巨细为m字节的而文件,当k>=m时,触发EOF。类似一个写操纵就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
内核开释文件打开时创建的数据布局,并将这个描述符规复到可用的描述符池中去。
- int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,而且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
fd是需要关闭的文件的描述符,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的实现分析
查看windows体系下的printf函数体:
图 53 printf的函数体
形参列表中的…是可变形参的一种写法,当通报参数的个数不确定时,用这种方式来表示。
va_list的定义:typedef char *va_list,说明它是一个字符指针,此中 (char*)(&fmt) + 4) 即arg表示的是...中的第一个参数。
再进一步查看windows体系下的vsprintf函数体:
图 54 vsprintf的函数体
则知道vsprintf步伐按照格式fmt结合参数args天生格式化之后的字符串,并返回字串的长度。
在printf中调用体系函数write(buf,i)将长度为i的buf输出。write函数如下:
printf函数的功能为继续一个格式化命令,并按指定的匹配的参数格式化输出,故i = vsprintf(buf, fmt, arg)是得到打印出来的字符串长度,其后的write(buf, i)是将buf中的i个元素写到终端。
因此,vsprintf的作用为继续确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数举行格式化,进而产生格式化输出。
再进一步对write举行追踪:
图 55 write的环境
这里给几个寄存器通报了参数,然后以一个int INT_VECTOR_SYS_CALL结束。INT_VECTOR_SYS_CALL代表通过体系调用syscall,查看syscall的实现:
图 56 syscall的环境
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码,符表现驱动子步伐:从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;
}
根据上面的getchar函数可知:
异步非常-键盘停止的处理:键盘停止处理子步伐。继续按键扫描码转成ascii码,保存到体系的键盘缓冲区。
getchar等调用read体系函数,通过体系调用读取按键ascii码,直到继续到回车键才返回。getchar的大概思想是读取字符串的第一个字符之后再举行返回操纵。
8.5本章小结
本章介绍了Unix I/O,通过LinuxI/O装备管理方法以及Unix I/O接口及函数相识体系级I/O的底层实现机制,并通过对printf和getchar函数的底层剖析加深对Unix I/O以及非常停止等的相识。
(第8章1分)
结论
将hello.c中include的所有外部的头文件头文件内容直接插入步伐文本中,完成字符串的替换,方便后续处理;
通过词法分析和语法分析,将正当指令翻译成等价汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s;
将hello.s汇编步伐翻译成呆板语言指令,并把这些指令打包成可重定位目标步伐格式,最终结果保存在hello.o 目标文件中;
通过链接器,将hello的步伐编码与动态链接库等收集整理成为一个单一文件,天生完全链接的可执行的目标文件hello;
打开Shell,在此中键入 ./hello 1190200208 李旻翀,终端为其fork新建进程,并通过execve把代码和数据加载入假造内存空间,步伐开始执行;
在该进程被调度时,CPU为hello其分配时间片,在一个时间片中,hello享有CPU全部资源,PC寄存器一步一步地更新,CPU不停地取指,顺序执行自己的控制逻辑流;
内存管理单元MMU将逻辑所在,一步步映射成物理所在,进而通过三级高速缓存体系访问物理内存/磁盘中的数据;
printf 会调用malloc 向动态内存分配器申请堆中的内存;
进程时候等待着信号,假如运行途中键入ctr-c ctr-z 则调用shell 的信号处理函数分别举行停止、挂起等操纵,对于其他信号也有相应的操纵;
Shell父进程等待并回收hello子进程,内核删除为hello进程创建的所有数据布局。
这一生,和人类类似,从出生到一点点成长,一步步长大,而且是一个老实听话的人,假如中途遇到其他事变,便会立刻的执行。等它忙碌了一辈子后,最终还是要入土为安,被回收走,不留一点陈迹。
感悟:
总体回首hello的一生,觉得计算机体系也是有生命的,这一生也充满了很多故事。这一生我们理解起来很艰难,但是我们可以通过对 hello一生的追溯来学习计算机对一个步伐的处理过程,有助于我们以后写出更好的代码,而且在对 hello 一步步操纵的学习中我们加深了对计算机体系的理解。
当做这个大作业时,彻底将看似孤立的每一章连了起来,知道了它们的前因后果,而且觉得这门课还是挺有意思的。其实,假如这个大作业能在开学初的时候布置下来,随着章节的学习一步步地做,可能会消化理解得更好一些。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中心产物的文件名,并予以说明起作用。
文件名
| 功能
| hello.i
| 预处理后得到的文本文件
| hello.s
| 编译后得到的汇编语言文件
| hello.o
| 汇编后得到的可重定位目标文件
| hello.elf
| 用readelf读取hello.o得到的ELF格式信息
| hello.asm
| 反汇编hello.o得到的反汇编文件
| hello2.elf
| 由hello可执行文件天生的.elf文件
| hello2.asm
| 反汇编hello可执行文件得到的反汇编文件
| (附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E.Bryant, David O'Hallaron. 深入理解计算机体系[M]. 呆板工业出版社.2018.4
[2] Pianistx.printf 函数实现的深入分析[EB/OL].2013[2021-6-9].
https://www.cnblogs.com/pianist/p/3315801.html.
[3] 空想之家xiao_chen.ELF文件头更具体布局[EB/OL].2017[2021-6-10].
https://blog.csdn.net/qq_32014215/article/details/76618649.
[4] Florian.printf背后的故事[EB/OL].2014[2021-6-10].
https://www.cnblogs.com/fanzhidongyzby/p/3519838.html.
(参考文献0分,缺失 -1分)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |