
计算机系统
大作业
题 目 程序人生-Hello's P2P
计算机科学与技术学院
2022年5月
摘 要
本文以hello这个最简单的程序回顾了计算机系统如何实现一个程序的运行,从hello这个程序运行的"一生"回顾计算机系统里的重要概念。本文主要涉及编译链接、进程控制、内存管理和IO。
关键词:计算机系统;P2P;编译;链接;进程;虚拟内存;IO
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
6.2 简述壳Shell-bash的作用与处理流程 - 30 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 37 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 39 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 40 -
7.7 hello进程execve时的内存映射 - 43 -
Hello的P2P是指program to process, 由程序到进程。也就是从我们编写的c语言程序到进程的过程。Hello.c 文件经过cpp预处理得到hello.i,而后经过ccl生成hello.s, 在之后经过汇编器as得到hello.o,之后链接器ld将其转换为hello可执行目标文件,最后shell为hello进行fork创建子进程,hello就成了process。
Hello的020即 from zero-0 to zero-0,说的是内存数据的从无到有再到无。初始时内存里没有hello的相关数据,因此是from zero-0,通过shell中的execute函数将hello载入内存为其分配空间,当程序结束后,hello进程被回收,内核删除内存里关于hello的数据,完成to zero-0.
本机:cpu:ryzen5800H RAM:3200MHz 16GB Samsung
虚拟环境:vmware16pro Ubuntu20.04
Halo.asm hello的反汇编
Halo.elf hello的elf格式
Hello hello程序的可执行程序
Hello.asm hello.o的反汇编
Hello.c 源程序
Hello.elf hello.o的elf格式
Hello.i hello的预处理文件
Hello.o hello的可重定位目标文件,为汇编后的结果
Hello.s hello的汇编文件
本章介绍hello从诞生到执行结束的过程,阐述了p2p和020的含义。
(第1章0.5分)
预处理是指预处理器(cpp,C Pre-Processor)根据以字符#开头的命令,修改原始的C程序的过程。
预处理的作用是由预处理器来进行注释删除、包含其他文件、以及执行宏替代,方便后续的编译。

Figure 1 预处理操作
打开经过预处理后的hello.i文件,发现文件有3060行,远远多余之前的程序,仔细观察发现#后include的头文件已经被插入其中,所有注释都已经删除。在hello.i
文件开始的几行内也记录了被插入的头文件的位置。

Figure 2 插入头文件的位置

Figure 3 预处理后产生的文件
本章对预处理的概念和作用进行了说明,并对hello.c进行了预处理操作以验证。
(第2章0.5分)
编译是指通过编译器将程序从便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的过程。
编译的作用是产生目标语言(target language),即汇编语言,为后续的汇编程序准备。
键入命令:gcc -m64 -Og -no-pie -fno-PIC hello.i -o hello.s -S,可生成hello.s文件。
Figure 4 编译命令
3.3.1 数据
程序中包含了字符串常量,在.s文件内如下


Figure 5 字符串常量
文件内也有进行for循环时使用的局部变量i,存在ebp寄存器内。

Figure 6 寄存器内的i变量
程序内的argc和argv作为main函数的参数,一般在edi和rdi内,在调用函数之前有寄存器传递参数。

Figure 7 edi存argc

Figure 8 rbx存argv地址,调用atoi时传递
3.3.2 赋值
在c语言程序中没有出现赋值 = 操作。
3.3.3 类型转换
C程序中未出现类型转换操作
3.3.4 sizeof
C语言程序中未出现sizeof
3.3.5 算数操作
For循环里每个循环对i都进行加1操作,在.s文件内如下

Figure 9 算数操作
3.3.6 逻辑/位操作
C语言源程序内没有逻辑/位操作。
3.3.7 关系操作
For循环每次进行判断应用了==关系操作,在文件内如下

Figure 10 关系操作
每个循环开始前与7比较大小进行==关系操作。
3.3.8 数据/指针/结构操作
在for循环的每个循环内需要对数据内元素进行访存操作,在文件内如下


Figure 11 通过寄存器内元素地址进行数组元素访存
3.3.9 控制转移
源程序中运用了for循环,在文件内如下

Figure 12 控制转换-for
同时程序内还含有if,在文件内如下

Figure 13 控制转移-if
3.3.10 函数操作
源程序调用了printf、atio、sleep、getchar、exit函数,在文件内如下

Figure 14 printf函数
寄存器Rdx,rsi,edi存储的都为printf函数的参数,eax为printf函数的返回值,在调用前置零。

Figure 15 atio函数
寄存器rdi存储着atio函数的参数,eax寄存器存储着该函数的返回值。

Figure 16 sleep函数
该函数的参数储存在edi寄存器内

Figure 17 调用getchar函数

Figure 18 调用exit函数
本章介绍了编译的概念和作用,编译为后续生成二进制机器码做准备。以生成的hello.s为例,分析了编译的结果。
(第3章2分)
使用汇编语言编写的源代码,然后通过相应的汇编程序(assembler)将它们转换成可执行的机器代码。这一过程被称为汇编过程。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
汇编的作用将汇编语言等价翻译为机器语言,生成可重定位目标程序。
输入的汇编命令为:gcc -m64 -Og -no-pie -fno-PIC hello.s -o hello.o -c

Figure 19 汇编过程
在终端内输入readelf -a hello.o > hello.elf指令以获得.elf文件,内容为hello.o的ELF格式:

Figure 20 hello.o的ELF格式
含有Magic、类别、数据、版本等系统基本信息

Figure 21 ELF头
包含节的名称、大小、地址、偏移量信息

Figure 22 节头

Figure 23 无程序头
包含了c语言中的两个字符串,puts、exit、printf、atoi、sleep、getchar函数。偏移量:本数据成员给出重定位所作用的位置。对于重定位文件来说,此值是受重定位作用的存储单元在节中的字节偏移量;对于可执行文件或共享ELF文件来说,此值是受重定位作用的存储单元的虚拟地址。
信息:本数据成员既给出了重定位所作用的符号表索引,也给出了重定位的类型。
类型:重定位到的目标类型。
加数:计算重定位位置的辅助信息。

Figure 24 重定位节
包含了偏移量、大小、类型、全局或者局部的信息,例如main函数就是偏移量为0的109字节全局函数。

Figure 25 符号表
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

Figure 26 生成反汇编文件
打开反汇编的文件,首先发现文件很小,只有main函数部分,与之前lab中拆炸弹的反汇编文件不一样,bomb的反汇编文件有许多系统的库函数的反汇编。通过与hello.s进行比较差异如下:
Hello.s内分支转移通过段.L1来转移,而反汇编内通过偏移量计算地址来进行转移

Figure 27 分支转移
Hello.s内函数调用直接call+函数名,而在反汇编里采用call+地址的方式进行函数调用,并且此时未经过链接器链接,文件内也没有库函数的反汇编,call之后的偏移量全为0,待链接器链接能够确定函数位置时call的偏移量便不再为0。

Figure 28 函数调用
在反汇编文件内常数表达为16进制,而在Hello.s内则为十进制。

Figure 29 24表示为16进制的0x18
本章回顾了编译的概念和作用,以hello.o为例分析验证编译的结果,比较反汇编文件和Hello.s文件的区别。
(第4章1分)
链接是指通过链接器将多个可重定位目标文件与库链接为一个可执行文件的过程。
链接将一个程序的整体氛围几个模块,让整个工程分散化不那么复杂,也方便修改和查看。
使用的命令: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

Figure 30 链接过程
使用❯ readelf -a hello > halo.elf生成halo.elf

Figure 31 生成ELF文件过程
分析文件的内容为
与hello.elf的基本相同,但是类型、入口点地址、程序头起点等存在不同。

Figure 32 ELF头
与hello.elf相比,节头内容更多,并且地址和对齐的信息发生改变。节头包含了名称、类型、地址、偏移量、大小、全体大小、旗帜、链接、信息、对齐的内容。


Figure 33 新的节头
相比于hello..elf没有程序头,halo.elf内包含了程序头。程序头表 (program header table) 是一个结构体数组,数组中的每个结构体元素是一个程序头 (program header),每个程序头描述一个段 (segment)。
相较于hello.elf多出了一个dynamic section,其中包含了动态库,以及需要立即加载与否和符号表。

Figure 34 dynamic section
重定位节与Hello.elf相比名称进行了变更,信息进行了更新。

Figure 35 重定位节
符号表更新了变得更长,保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中有声明。

Figure 36 符号表
使用symbols查看,发现hello加载时内存里各段的地址与elf表内地址相同,例如.interp段。

Figure 37 使用symbols查看
经过链接后的hello多了许多内容,具体如下。

Figure 38 objdump查看反汇编

Figure 39 新增的函数反汇编

Figure 40 函数调用的不同

Figure 41 跳转的不同

Figure 42 程序名称
编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,在GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。
在elf节头内可知.got.plt在地址00404000.

Figure 43 函数调用前

Figure 44 函数调用后
可以看见.got.plt节内容发生了更新。
本章回顾了链接的概念和作用,并且得到了可执行文件,然后将可执行文件的elf格式以及反汇编同hello.o的elf格式和反汇编进行比较,进而验证了链接的作用加深了理解。
(第5章1分)
进程是一个正在运行的程序的实例。(In computing, a process is the instance of a computer program that is being executed by one or many threads.)
进程为程序提供了两个抽象:
Shell(也称为壳层)在计算机科学中指"为用户提供用户界面"的软件,通常指的是命令行界面的解析器。一般来说,这个词是指操作系统中提供访问内核所提供之服务的程序。Shell也用于泛指所有为用户提供操作界面的程序,也就是程序和用户交互的层面。因此与之相对的是内核(英语:Kernel),内核不提供和用户的交互功能。
Shell的处理流程为读取命令,分析命令参数,识别是否为内置命令,判断正误,执行。

Figure 45 shell的流程
打开shell输入./hello 7203610404 彭癸龙 1, hello后是该执行该文件的参数。Shell会识别该命令行为非内置命令,之后在当前目录下用输入的参数执行程序,这是用fork函数创建子进程实现的,子进程和父进程有相同的进程上下文,但是pid不同,父进程可以回收子进程。当父进程不存在而子进程需要回收时则通过init进程回收僵尸子进程。
Figure 46 shell内运行hello
在创建子进程后会通过execve函数载入我们编写的hello程序。加载器删除子进程原始的上下文(上下文这时和shell相同),并初始化新的进程上下文。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后设置PC指向程序入口点,完成对可执行程序的execve加载过程,这时进程上下文就是hello程序的进程上下文。
在程序运行时,Shell为hello fork了一个子进程,这个子进程与Shell有独立的逻辑控制流。在执行sleep函数之前一直是hello的时间片,hello的进程也不会被抢占。当hello调用sleep函数时,其效果是进入内核模式,内核处理之后释放当前hello进程切换为其他进程。同时将 hello 进程从运行队列加入等待队列,并开始由计时器进行计时。当计时达到参数的要求时触发异常控制流,进入内核模式。系统从当前进程切换为等待序列中的hello进程,进行了进程上下文切换,又由内核模式转为用户模式。此时 hello 进程执行逻辑控制流。
正常运行的结果:
Figure 47 不输入时正常运行
在程序执行时键入回车,程序仍然正常执行,只是中间有空行。
Figure 48 键入回车
当键入ctrl+c时程序提前结束了,这是因为shell进程收到了SIGSTP信号,信号处理程序将hello进程回收。
Figure 49 键入ctrl+c提起停止
当输入ctrl+z时程序被挂起。
Figure 50 键入ctrl+z挂起
然后键入ps jobs pstree fg kill命令,结果如下。
Figure 51 键入ps
Figure 52 键入jobs
Figure 53 键入pstree展示进程树
Figure 54 键入kill杀死进程
Figure 55 不停乱按
本章回顾了进程的概念和作用,和shell的概念和作用。以hello为例分析进程,在shell中运行hello。并且在执行进程中通过尝试键入不同的输入达到从逻辑控制流到异常控制流的效果,验证了信号和异常控制流的知识。
(第6章1分)
1. 逻辑地址
逻辑地址是指由程序产生的与段相关的偏移地址,逻辑地址由选择符和偏移量两部分组成。具体而言,其为hello.asm中的相对偏移地址。
2. 线性地址
逻辑地址经过段机制转化后为线性地址,方便处理器进行寻址,用于描述程序分页信息的地址。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。
3. 虚拟地址
即为上述线性地址。
4. 物理地址
CPU通过地址总线的寻址,找到真实的物理内存对应地址。
英特尔为了解决巨大的内存空间问题,引入了一个中间结构体,段描述符。并增设两个寄存器GDTR和LDTR。段描述符占8个字节定义如下。
通过段描述符,我们能够得到如下信息:
段的基址,由B31-B24/B23-B16/B15-B0构成,一共32位,基址可以是4GB空间中任意地址;
段的长度,由L19-L16/L15-L0构成,一共20位。如果G位为0,表示段的长度单位为字节,则段的最大长度是1M,如果G位为1,表示段的长度单位为4kb,则段的最大长度为1M*4K=4G。假定我们把段的基地址设置为0,而将段的长度设置为4G,这样便构成了一个从0地址开始,覆盖整个4G空间的段。访存指令中给出的"逻辑地址",就是放到地址总线上的"物理地址",这有别于"段基址加偏移"构成的"层次式"地址。
段的类型,代码段还是数据段,可读还是可写

Figure 56 段描述符定义
十六位段寄存器的称之为段选择符,其定义如下

Figure 57 段选择符定义
通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。
概念上而言, 虚拟内存被组织为一个由存放在磁盘上的 N 个连续的字节大小的单元组成的数组。 每字节都有一个唯一的虚拟地址, 作为到数组的索引。 磁盘上数组的内容被缓存在主存中。 和存储器层次结构中其他缓存一样, 磁盘( 较低层) 上的数据被分割成块,这些块作为磁盘和主存( 较高层) 之间的传输单元。 VM 系统通过将虚拟内存分割为称为虚拟页( Virtual Page, VP)的大小固定的块来处理这个问题。每个虚拟页的大小为 P= 2〃 字节。 类似地, 物 理 内 存 被 分 割 为 物 理 页( Physical Page, PP ), 大小也为 P 字节( 物理页也被称为页帧( page frame))。同任何缓存一样, 虚拟内存系统必须有某种方法来判定一个虚拟页是否缓存在DRAM 中的某个地方。这些功能是由软硬件联合提供的, 包括操作系统软件、 MMU(内存管理单元) 中的地址翻译硬件和一个存放在物理内存中叫做页表(page table)的数据结构 ,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。操作系统负责维护页表的内容,以及在磁盘与 DRAM 之间来回传送页。页表就是一个页表条目( Page Table Entry,PTE)的数组。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个 PTE。有效位表明了该虚拟页当前是否被缓存在 DRAM 中。 如果设置了有效位,那么地址字段就表示 DRAM 中相应的物理页的起始位置, 这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。

Figure 58 页表
TLB 是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个 PTE 组成的块。TLB 通常有高度的相联度。
图59展示了当 TLB 命中时(通常情况)所包括的步骤。这里的关键点是,所有的地址翻译步骤都是在芯片上的 MMU 中执行的,因此非常快。

Figure 59 TLB命中
当 TLB 不命中时,MMU必须从L1缓存中取出相应的PTE,如图60所示。新取出的PTE 存放在TLB 中,可能会覆盖一个已经存在的条目。

Figure 60 TLB不命中
图61描述了使用k级页表层次结构的地址翻译。虚拟地址被划分成为k个 VPN 和1 个 VPO。每个 VPN i都是一个到第i级页表的索引其中1<=i<=k,第j级页表中的每个PTE,1<=j<=k-1,都指向第 j+1 级的某个页表的基址。第k级页表中的每个 PTE 包含某个物理页面的 PPN, 或者一个磁盘块的地址。为了构造物理地址,在能够确定 PPN之前,MMU 必须访问k个 PTE。对于只有一级的页表结构, PPO 和 VPO 是相同的。访问k个 PTE, 第一眼看上去昂贵而不切实际。然而,这里 TLB 能够起作用,正是通过将不同层次上页表的 PTE 缓存起来。实际上,带多级页表的地址翻译并不比单级页表慢很多。

Figure 61 多级页表加速地址翻译
使用物理寻址,多个进程同时在高速缓存中有存储块和共享来自相同虚拟页面的块成为很简单的事情。而且高速缓存无需处理保护问题,因为访问权限的检查是地址翻译过程的一部分。图 62展示了一个物理寻址的高速缓存如何和虚拟内存结合起来。主要的思路是地址翻译发生在高速缓存查找之前。如果在一级cache中命中PA对应的块,就根据CO将要找的数据传给cpu。如果在一级cache中找不到对应的数据,就得根据相同的地址去二级,三级甚至主存中寻找数据。在二,三级cache中找到数据还需要在一级cache中根据具体情况选择是否要进行驱逐和替换。

Figure 62
当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据结构,并分配给它一个唯一的PID。为了给这个新的hello创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
execve函数加载并运行hello需要以下几个步骤:
1. 删除已存在的用户区域
删除当前进程hello虚拟地址的用户部分中的已存在的区域结构。
2. 映射私有区域
为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域结构。其中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
3. 映射共享区域
若hello程序与共享对象或目标链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。
4. 设置程序计数器
最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
图 63 展示了在缺页之前我们的示例页表的状态。CPU 引用了VP 3中的一个字VP 3并未缓存在DRAM中.地址翻译硬件从内存中读取 PTE 3 , 从有效位推断岀 VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在 PP 3 中的 VP 4。如果 VP 4 已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP 4的页表条目,反映出 VP 4 不再缓存在主存中这一事实。

Figure 63 缺页之前
接下来,内核从磁盘复制 VP 3 到内存中的 PP 3, 更新 PTE 3, 随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP 3已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了。图 64展示了在缺页之后我们的示例页表的状态。

Figure 64 缺页之后
动态内存管理一般由动态内存分配器维护,分配器有两种风格,显示和隐式,两者的区别在于前者要求应用显式的释放任何已分配块,而后者则自动检测一个已分配块何时不再被程序使用。不过两者都要求显式地分配空闲块。
C 标准库提供了一个称为 malloc 程序包的显式分配器。程序通过调用 malloc 函数来从堆中分配块。动态内存分配器, 例如 malloc, 可以通过使用 mmap 和 munmap 函数, 显式地分配和释放堆内存,或者还可以使用 sbrk 函数。
我们可以将堆组织为一个连续的已分配块和空闲块的序列的结构成为隐式空闲链表。分配器可以通过遍历堆中所有的块, 从而间接地遍历整个空闲块的集合。隐式空闲链表的优点是简单。显著的缺点是任何操作的开销,例如放置分配的块, 要求对空闲链表进行搜索,该搜索所需时间与堆中已分配块和空闲块的总数呈线性关系。很重要的一点就是意识到系统对齐要求和分配器对块格式的选择会对分配器上的最小块大小有强制的要求。
本章回顾了虚拟内存管理,如何用段式管理实现虚拟地址到物理地址的翻译,如何使用缓存、多级页表、TLB加速这个翻译过程。之后讨论了hello在fork和execve后的内存映射。
(第7章 2分)
一个 Linux 文件就是一个 771 个字节的序列。所有的 I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为UnixI/O, 这使得所有的输人和输出都能以一种统一且一致的方式来执行。
1. 接口
1. 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2. Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为 0)、标准输出(描述符为 1)和标准错误(描述符为 2)。头文件< unistd.h> 定义了常量 STDIN_FILENO、STDOUT 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。
2.函数
int open(char *filename, int flags, mode_t mode);
open 函数将 filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。
int close(int fd);
ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, const void *buf, size .t n);
read 函数从描述符为 fd 的当前文件位置复制最多 n 个字节到内存位置 buf。返回值-1表示一个错误,而返回0表示EOF。否则,返回值表示的是实际传送的字节数量。
write 函数从内存位置 buf 复制至多n个字节到描述符 fd 的当前文件位置。
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;
}
在形参列表里有这么一个token:...,这个是可变形参的一种写法。当传递参数的个数不确定时,就可以用这种方式来表示。va_list的定义:typedef char *va_list这说明它是一个字符指针。其中的: (char*)(&fmt) + 4) 表示的是...中的第一个参数。
再看Windows下的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);
}
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
再看printf里的write
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
给几个寄存器传递了参数,然后以一个int INT_VECTOR_SYS_CALL结束。INT_VECTOR_SYS_CALL代表通过系统调用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
通过直接写显存的方式输出字符串。
Getchar调用系统函数read,发送中断信号,内核进而抢占进程,等待用户输入字符串,输入字符和回车后,这些都保存到缓存区内,然后再次发送信号,进行上下文切换重新回到进程的逻辑控制流,getchar从缓存区读取字符串。
本章介绍了linux系统关于IO的接口和函数,之后对printf和getchar进行解析。
(第8章1分)
Hello的程序进程基本过程同大作业的章节顺序一样。
从program到process,经历了预处理、编译、汇编、链接、加载运行的过程。而从存储空间的角度,execve将进程对应的代码和数据加载如虚拟内存空间,程序以此为基础执行,经历执行指令和访存的过程,在printf函数出还会调用malloc,同时逻辑流会被异常流打断,这是为了处理信号,最后hello被父进程回收,内核会回收内存里hello的数据,这便是020.
我认为计算机系统是个相对抽象的系统,他的设计和实现运用了挺多抽象和创造概念,也运用了类似流水线这种现实中非常使用的概念,因而计算机系统是个综合而抽象的系统。同时作为一个系统它非常具有体系和结构,从最基础的数据的存储和处理,再到cpu流水线和访存、存储结构,再到系统与人的交互,每个部分都考虑周全。
(结论0分,缺失 -1分,根据内容酌情加分)
Halo.asm hello的反汇编
Halo.elf hello的elf格式
Hello hello程序的可执行程序
Hello.asm hello.o的反汇编
Hello.c 源程序
Hello.elf hello.o的elf格式
Hello.i hello的预处理文件
Hello.o hello的可重定位目标文件,为汇编后的结果
Hello.s hello的汇编文件
(附件0分,缺失 -1分)
为完成本次大作业你翻阅的书籍与网站等
https://www.cnblogs.com/pianist/p/3315801.html
https://zh.wikipedia.iwiki.eu.org/wiki/%E6%AE%BC%E5%B1%A4
https://blog.csdn.net/ZekeJaeger/article/details/103766827
(参考文献0分,缺失 -1分)
文章目录前言一、插入背景二、头部1.导航栏2.优化导航栏3时间4.搜索框三、主体四、底部五、背景泡沫球特效六、note小便签七、全部代码1.index.html2.style.css3.index.js八、总结链接:https://pan.baidu.com/s/1uaRCJXyIrY56NXabau4wjw?pwd=LDL6提取码:LDL6前言咕咕了好久啦,今天使用原生HTML+CSS+JS做一个很简单的个人定制的导航主页吧!先看下完整的效果图吧!接下来的文章将逐步带领大家制作,现在太晚了,就精简了下,删除了部分动画效果,项目整体非常简单!项目地址:链接:https://pan.baidu.
2022/2023学年第一学期课程设计实验报告模块名称Android课程设计专业通信工程(嵌入式培养)学生班级学生学号学生姓名指导教师设计题目熟悉adt-bundle-windows-x86或android-studio-ide应用开发环境:安装建立adt-bundle-windows-x86或android-studio-ide的应用开发环境实验。能编写基于移动端的android应用程序基本的界面及部分应用框架的程序设计综合应用任务要求1.熟悉adt-bundle-windows-x86开发环境安装与配置,能编写基于移动端的android应用程序掌握最基本的项目创建方法。掌握项目中的文件构成
数据库课程设计大作业大盘点【建议在校生收藏】在学校之中学习数据库是一个充满挑战的学科,一个初学者第一次将自己的项目与数据库相结合。本文介绍了40多个数据库课程设计的大作业并实现了网页端的图形化界面,其中主要采用SQLServer作为主要开发工具,如果采用MySQL、Oracle、SQLlite等数据库也具有参看意义。详情请点击对应的链接查看。打包下载以下所有内容38个数据库课程设计+SQLServer.zip单独下载地址《数据库课程设计》_SQLServer社区水电费管理信息系统.doc-数据库文档类资源-CSDN下载《数据库课程设计》_大作业仓库管理系统设计与开发.docx_数据库课程设计
相关链接Python大作业——爬虫+可视化+数据分析+数据库(简介篇)Python大作业——爬虫+可视化+数据分析+数据库(爬虫篇)Python大作业——爬虫+可视化+数据分析+数据库(数据分析篇)Python大作业——爬虫+可视化+数据分析+数据库(数据库篇)一、登录界面由于该程序会通过与数据库的交互来实现歌曲收藏等功能,故需要首先设计一个进行登录注册的界面登录界面将与主界面同大小,且为了方便布局,设置为固定大小不可改变self.setFixedSize(960,700)self.setWindowTitle('登录')#设置窗口名称self.setWindowIcon(QIcon('fav
实验报告实验(三)题 目 优化 专 业 人工智能(未来技术) 学 号 7203610716 班 级 20WJ102 学 生 孙铭蔚 指导教师 刘宏伟 实验地点 G712 实验日期 2022.04.16 计算学部目 录第1章实验基本信息.......................................-3-1.1实验目的.
1、背景W餐饮外卖平台向广大用户提供网上订餐服务,其市场占有量在近年不断增加。当用户在W平台订餐完成后,平台会引导用户对于品尝过的菜品进行评价打分,最高为5分,最低为1分。通过用户的评分数据,可以分析外卖平台的受欢迎度、客户的体验度。数据说明用户评分数据(mealrating.txt)属性名称属性说明UserID用户IDMealID菜品IDRating评分ReviewTime评分的时间戳Review评价内容菜品数据集(meal_list.txt) 2、任务将用户评分数据和菜品数据导入Hive根据用户评分数据统计日销量和日用户量selectcount(1)frommealratingwhereR
提示:各种渲染效果的切换需要手动在片段着色器中切换FragColor的赋值函数目录简介效果图一、开发流程1.环境配置2.框架搭建二、基础功能1.图形绘制2.着色器3.纹理材质 4.摄像机 三.核心功能1.模型加载 2.光照算法四.高阶功能1.帧缓存2.特殊材质效果-环境映射3.阴影渲染 总结简介本OpenGL程序旨在通过OpenGL从底层出发,准许现实世界的真实物理表现,通过OpenGL实现一个基于物理的初级渲染管线。我们可以在OpenGL中自由的创建出我们想要的Model,然后也可以自由的为其添加材质贴图,最后我们通过着色器将我们的模型与材质数据输入按照一定的物理法则进行处理,进而实现在计算
作者|王瑞平审校|云昭51CTO读者成长计划社群招募,咨询小助手(微信号:TTalkxiaozhuli)最近出现的人工智能产品五花八门、功能各异,从文本输出到绘画、视频、音频。这些人工智能工具将艺术创作变得形象生动,展现出用AI描绘的另一个世界。绘画作为一种熟悉而有趣的表达方式,能够将生活变得色彩斑斓。在创作绘画的同时,我们同样希望将讨人喜欢的静态角色变成动画,使其能够在书页上“走动”,甚至还有书籍和电视剧讲述了这种幻想。不幸的是,实现这样的效果相当困难!通过创建图形产生运动错觉(如,翻页书)呈现出的效果往往很乏味,新用户使用现有的动画工具又很困难。因此,许多绘画角色仍在纸面上保持静态。前不久
主要目的是为了学习Scrapy与Sklearn而不是写论文,结论是瞎扯的,轻喷求求了目录摘要数据爬虫程序设计和实现Scrapy框架Scrapy框架简介Scrapy的组件Scrapy的工作过程爬取豆瓣TOP250网页新建爬虫项目编写爬虫程序获得csv文件数据分析与挖掘算法的设计和实现NumPy&Pandas&Scikit-learn简介NumPyPandasScikit-learn用三方库处理爬虫文件读取文件处理数据算法实现数据可视化设计和实现Matplotlib简介可视化设计结论附录摘要在我们的日常生活中,电影已经成为了我们娱乐放松活动所不可缺少的元素。然而,自电影诞生以来,人们每天都在生产着