HIT盘算机系统大作业-程序人生

打印 上一主题 下一主题

主题 568|帖子 568|积分 1708

摘  要

  从一个简朴的C语言代码,到可运行文件运行竣事的过程中,充满了很多时间被隐藏起来封装好的步骤。我们很多时间都已经风俗了开箱即用,而忽视了我们所使用的程序内部是什么样的。此论文将会从各个方面讲解有关于Hello world程序的各个部门,那些在后台,乃至在暗处不为人所知的步骤。
  关键词盘算机系统;                         
  
  (择要0分,缺失-1分,根据内容精彩称都酌情加分0-1分
  

  
  

  

  

  

  

  

  
目  录

  

  
第1章 概述

  
1.1 Hello简介

  
1.2 环境与工具

  
1.3 中间效果

  
1.4 本章小结

  
第2章 预处理

  
2.1 预处理的概念与作用

  
2.2在Ubuntu下预处理的下令

  
2.3 Hello的预处理效果剖析

  
2.4 本章小结

  
第3章 编译

  
3.1 编译的概念与作用

  
3.2 在Ubuntu下编译的下令

  
3.3 Hello的编译效果剖析

  
3.4 本章小结

  
第4章 汇编

  
4.1 汇编的概念与作用

  
4.2 在Ubuntu下汇编的下令

  
4.3 可重定位目标elf格式

  
4.4 Hello.o的效果剖析

  
4.5 本章小结

  
第5章 链接

  
5.1 链接的概念与作用

  
5.2 在Ubuntu下链接的下令

  
5.3 可执行目标文件hello的格式

  
5.4 hello的假造地点空间

  
5.5 链接的重定位过程分析

  
5.6 hello的执行流程

  
5.7 Hello的动态链接分析

  
5.8 本章小结

  
第6章 hello历程管理

  
6.1 历程的概念与作用

  
6.2 简述壳Shell-bash的作用与处理流程

  
6.3 Hello的fork历程创建过程

  
6.4 Hello的execve过程

  
6.5 Hello的历程执行

  
6.6 hello的非常与信号处理

  
6.7本章小结

  
第7章 hello的存储管理

  
7.1 hello的存储器地点空间

  
7.2 Intel逻辑地点到线性地点的变更-段式管理

  
7.3 Hello的线性地点到物理地点的变更-页式管理

  
7.4 TLB与四级页表支持下的VA到PA的变更

  
7.5 三级Cache支持下的物理内存访问

  
7.6 hello历程fork时的内存映射

  
7.7 hello历程execve时的内存映射

  
7.8 缺页故障与缺页中断处理

  
7.9动态存储分配管理

  
7.10本章小结

  
第8章 hello的IO管理

  
8.1 Linux的IO装备管理方法

  
8.2 简述Unix IO接口及其函数

  
8.3 printf的实现分析

  
8.4 getchar的实现分析

  
8.5本章小结

  
结论

  
附件

  
参考文献

  
  

  第1章 概述

  1.1 Hello简介

  根据Hello的自白,利用盘算机系统的术语,简述Hello的P2P,020的整个过程。
  



      • P2P


  Hello程序的代码(Program)经过大致下面步骤编程一个可执行文件的。首先从hello.c文件开始,先通过预处理器得到修改了的源程序hello.i;然后,编译器(cc1)将其转化为汇编文件hello.s;接着,汇编器(as)将汇编文件转化为一个可重定位目标程序hello.o,接下来,链接器将一些文件与hello.o归并,得到hello。这些文件通常是标准C库中的函数对应的,预编译好了的目标文件。当我们运行时,shell会用fork()函数创建子历程,再用execve加载hello程序,这时,hello就由程序变成了一个历程(Process),完成了P2P的过程
  



      •  020


  开始时,hello并没有运行在系统中,相当于0。在shell中调用fork()函数创建子历程,再用execve加载可执行目标程序hello,映射假造内存。程序开始时载入物理内存,进入CPU处理。CPU为执行文件hello分配时间片,举行取值、译码、执行等流水线操纵。内存管理器和CPU在执行过程中通过L1、L2、L3三级缓存和TLB多级页表在物理内存中存取数据,通过I/O系统举行输出。程序运行竣过后,shell父历程会将该子历程举行回收,内核把它从系统中清除,这时hello就由0转化为了0,完成了020的过程
  
  1.2 环境与工具

  列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
  1.2.1.  硬件环境
  处理器 Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz   1.19 GHz
  机带RAM 16.0 GB (15.8 GB 可用)
  系统类型  64 位操纵系统, 基于 x64 的处理器
  1.2.2.  软件环境
  VMWare 17.5.0 build-22583795
  Ubuntu 22.04.3 LTS64位
  1.2.3.  开发工具
  Visual Studio 2022 64位;CodeBlocks 64位;vi/vim/gedit+gcc;
  
  1.3 中间效果

  列出你为编写本论文,生成的中间效果文件的名字,文件的作用等。
  1.4 本章小结

  本章主要是概述内容,先容了hello程序的P2P和020过程,也先容了完成此次大作业所使用的硬件环境、软件环境以及开发工具。还列出了为完成本次大作业,生成的中间效果文件等。
  
(第1章0.5分)

  
  

  第2章 预处理

  2.1 预处理的概念与作用

  2.1.1. 预处理的概念
  预处理是指预处理器根据预处理下令(以#开头),修改原始的C挨骂。好比hello.c中第一行中的#include <stdio.h>下令告诉预处理器读取系统头文件stdio.h的内容,并直接插入到程序中。效果是得到了另一个C代码文件,以.i作为拓展名
  2.1.2. 预处理的作用
  1. 举行宏替换,例如替换掉#define所定义的宏。
  2. 处理文件包罗,将#include中包罗的文件的内容插入到程序文本中。
  3. 按照宏定义,使用#if 、#elif 、#else等,举行有选择的操纵。
  4. 删除C语言源程序中所有的解释
  2.2在Ubuntu下预处理的下令

  cpp hello.c -o hello.i
  

  
  2.3 Hello的预处理效果剖析

  对比发现,hello.i中的代码相对于hello.c中只有几十行的代码量,到达了上千行。文件最后是去掉了解释的main()函数内容,上面要么是以#号开头的预处理内容,要么是引入的头文件内容
  

  

  
  2.4 本章小结

  本章先容了概念、功能等有关于预处理的内容,尝试预处理得到源程序的预处理效果,并对其举行了分析。
  
  
(第2章0.5分)

  
第3章 编译


  3.1 编译的概念与作用

  
  3.1.1. 编译的概念
  编译是指将便于人编写、阅读、维护的高级盘算机语言所写作的源代码程序,翻译为盘算机能解读、运行的低阶机器语言的程序。在这里指编译器(cc1)将文本文件hello.i翻译成汇编语言程序hello.s的过程。
  3.1.2. 编译的作用
  编译的作用是将高级盘算机语言所写作的源代码程序翻译为汇编语言程序,在这个过程中,会举行以词法分析、语法分析、语义分析来生成汇编语言程序,且编译器大概在这个过程中根据编译选项对程序举行一些适当的优化。
  3.2 在Ubuntu下编译的下令

  cc1 hello.i -o hello.s
  

  
  3.3 Hello的编译效果剖析

  3.3.1. 数据
  3.3.1.1 数字常量
  在这里,将argc与4比较,数字常量4在hello.s中以立即数$4的方式出现。
  

  在循环的判断条件内出现了数字常量7,。由于循环条件为小于8,编译器将其转化为小于等于7,在截图倒数第二行中。
  

  3.3.1.2. 字符串常量
  在hello.c文件中,有两个字符串,均出如今printf()函数内。
  在汇编代码中,这两个字符串被存储在内存中的.rodata节常量区中
  

  在调用printf()函数时,编译器将语句翻译为rip寄存器的偏移量形式表现,并将该偏移量存入rdi寄存器中,作为函数的第一个参数调用。
  

  
  3.3.1.3. 局部变量
  编译器通常将局部变量放在寄存器中或者栈中。
  例如将寄存在寄存器edi中的int argc参数,放在-20(%rbp)位置(第21行)。将char** argv放在-32(%rbp)位置(第22行)
  

  将局部变量int i放在栈中-4(%rbp)的位置(第30行)
  3.3.2 赋值
  在hello.c文件中,对i赋值0对应的汇编代码如下
  

  3.3.3. 函数调用
  这里只以一处函数调用为例。在hello.c文件中,调用atoi()函数的过程如下。
  

  首先将%rdi的值设为argv[3],如许atoi()函数的第一个参数就是argv[3];然后调用atoi()函数
  3.3.4. 算术操纵
  hello.c中,for循环中i++;语句对变量举行了算术操纵。
  

  这里执行了i的自增操纵。
  3.3.5. 关系操纵
  代码中涉及到了两处比较。但这里只用其中一处比较举例。
  

  在这里,argc与4比较,对应的汇编代码已经在截图有标示。
  3.3.6. 指针操纵
  

  在这里,对数组的访问变为3步。1. 取数组的首地点。2. 在首地点上加上偏移量,获得数组元素的地点。3. 访问该数组元素的地点。
  3.3.7. 控制转移
  这里的代码涉及到了多处跳转,分别如下
  

  
  3.4 本章小结

  本章主要先容了编译的概念,以及编译的作用和功能。详细分析了不同的C语言代码会编译为什么样的汇编代码。
  
第4章 汇编


  4.1 汇编的概念与作用

  4.1.1. 汇编的概念
  汇编是指将汇编语言程序经过编译器转化为二进制的机器语言指令,并将这些指令打包为可重定位目标程序的格式,并保存在目标文件.o中。
  4.1.2. 汇编的作用
  汇编的作用是把汇编语言翻译成机器语言,用二进制0和1代替汇编语言中的符号,即让它成为机器可以直接辨认的程序。最后把这些指令打包成可重定位目标程序的格式,并保存在目标文件.o中
  4.2 在Ubuntu下汇编的下令

  

  4.3 可重定位目标elf格式

  4.3.1. ELF头
  hello.o的ELF格式的开头是一个以16字节的序列开始的ELF头。这个序列表述了生成该文件的系统信息,例如字的巨细和字节次序。ELF头剩下的部门资助链接器举行语法分析息争释目标文件的信息,包罗ELF头的巨细、目标文件的类型、处理器体系布局、节头部表的文件偏移,以及节头部表中条目的巨细和数目。
  

  4.3.2. 节头部表
  文件的节头部表形貌了目标文件中的不同节的类型、地点、巨细、偏移等信息,以及可以对各部门举行的操纵权限。
  

  4.3.3. 重定位节
  重定位节中包罗了.text 节中需要举行重定位的信息,我们可以发现需要重定位的函数有: .rodata, puts, exits, printf, atoi, sleep, getchar
  
  

  4.3.4. 符号表
  符号表存放了程序中定义和引用的函数和全局变量的信息。
  

  
  
  4.4 Hello.o的效果剖析

  

  反汇编代码中,hello.s和hello.i中的代码也有少些不同。例如jmp指令,在.s文件中表现为jmp .L3,而在.o文件中,表现为jmp 80(用详细指令地点代替label)
  再好比调用函数也有区别。
  

  在.o文件中的机器码对应的操纵数为0。这是因为重定向后再链接生成可执行文件后才会生成确定的地点。所以这里的相对地点都用0代替。
  4.5 本章小结

  本章举行了hello.s的汇编,将其转换为二进制可重定位目标程序文件hello.o,通过readelf读取其elf信息与重定位信息,得到其符号表的相关信息,并通过objdump反汇编目标文件,从中得到机器代码,并将机器代码与汇编代码举行对照,发现机器语言与汇编语言存在一一映射关系。
  
  
5链接


  5.1 链接的概念与作用

  5.1.1. 链接的概念
  将文件中调用的各种函数跟静态库及动态库链接,并将它们打包归并形成可执行文件的过程。
  5.1.2. 链接的作用
  链接完成符号剖析和重定位,符号剖析将代码中的每个符号引用和一个符号定义关联起来。重定位归并输入模块,并为每个符号分配运行时地点。
  5.2 在Ubuntu下链接的下令

  

  5.3 可执行目标文件hello的格式

      hello与hello.o的elf头大致相同。不同支持在于hello的类型为EXEC可执行文件,并且有26个头。
  

  其各段根本信息如下
  

  

  
  5.4 hello的假造地点空间

   
  

  假造地点段从0x401000到0x402000,与节头部表的.init相符。
  5.5 链接的重定位过程分析

  这里对比hello.o与hello的反汇编代码的区别。

  hello.o反汇编代码的地点从0开始,而hello的反汇编代码从0x400000开始。这说明hello.o还未实现重定位,采取的是相对偏移地点,而hello已经实现了重定位,采取的是假造地点。
  

  重定位后,重定位节和符号定义链接器将所有一直的节归并,并赋予新的地点。如许程序中每条指令和全局变量都有唯一一个运行时地点。如果遇见未知目标的引用,那么就新生成一个重定位条目。
  5.6 hello的执行流程

  

  通过edb的调试纪录可知,每次调用call下令进入的函数如下所示。
  <_init> <.plt> <printf@plt> <atoi@plt> <_start> <_dl_relocate_static_pie>  <libc_csu_init> <exit@plt> <puts@plt> <getchar@plt> <sleep@plt> <main> <_fini> <__libc_csu_fini>
  
  5.7 Hello的动态链接分析

  查阅节头可知,.got.plt起始位置为0x404000。在调用dl_init前后,got[1]和got[2]的内容发生了变革。

  5.8 本章小结

  本章分析了可执行文件hello的ELF格式及其假造地点空间,分析了重定位过程、加载以及运行时函数调用次序以及动态链接过程,深入理解了链接和重定位的过程。
  
  
  
6hello历程管理


  6.1 历程的概念与作用

  6.1.1. 历程的概念
  历程是一个执行中的程序的实例,是系统举行资源分配和调治的根本单元
  6.1.2. 历程的作用
  历程提供独立的逻辑控制流,好像程序在独占地使用处理器一样。也提供一个私有的地点空间,好像我们的程序独占地使用内存系统。
  6.2 简述壳Shell-bash的作用与处理流程

  
6.2.1. 壳Shell-bash的作用

  
Shell为用户提供下令行界面,使用户可以在这个界面中输入shell下令,然后shell执行一系列的读/求值步骤,读步骤读取用户的输入的下令行,求值步骤则剖析下令行,并运行程序。完成后重复上述步骤,直到用户退出shell。从而完成用户与盘算机的交互来操纵盘算机。

  
6.2.2. 壳Shell-bash的处理流程

  
Shell等待用户输入指令。在用于输入指令后,从终端读取该下令并举行剖析。若该下令为shell的内置下令,则立即执行该下令,否则,shell会通过fork创建一个子历程,通过execve加载并运行该可运行目标文件,用waitpid下令等待执行竣事并对其举行回收,从内核中将其删除。若将该文件转为后台运行,则shell返回到循环的顶部,等待下一条下令。完成上述过程后,shell重复上述过程,直到用户退出shell。

  6.3 Hello的fork历程创建过程

  shell会调用fork函数创建一个子历程,内核会为新历程创建各种数据布局,并分配给它一个唯一的pid。新历程的假造内存与调用fork的历程的假造内存相同
  6.4 Hello的execve过程

  execve函数会根据文件名加载并运行程序。在假造内存中,它会删除用于地区,并映射私有地区,同时会设置程序计数器,
  6.5 Hello的历程执行

  当开始运行hello时,内存为hello分配时间片。如果一个系统运行多个历程,那么处理器的一个物理控制流被分成了多个逻辑控制流。逻辑流交织使用处理器。如果在此期间发生了非常或者系统中断,则内核会休眠该历程,并在焦点态中举行上下文切换,将控制权交给其他历程。当hello执行到sleep时,hello会休眠,再次上下文切换,控制交付给其他历程。一段时间后再次上下文切换,规复hello在休眠前的上下文信息,控制权交给hello继续执行。

  hello在执行循环语句后,程序调用getchar(),hello从用户态进入焦点态,并再次上下文切换,控制交付给其他历程。最终,内核从其他历程回到hello历程,在return后历程竣事。
  6.6 hello的非常与信号处理

  6.6.1. 正常执行
  正常执行时,hello每隔一秒打印一次,进入循环,共打印8次。打印完毕后,等待用户输入回车后程序终止。shell回收hello子历程,继续等待用户输入指令。
  

  
  6.6.2. 不停乱按
  乱按对于程序运行没有影响
  

  
  6.3.3. 按ctrl+z
  按下后,产生中断非常,此时hello的父历程shell吸收到SIGSTP信号,运行信号处理程序。最终hello被挂起。
  
  

  6.3.3.1. 输入ps
  在此之后输入ps,打印出各个历程的pid,其中包罗被挂起的hello。
  6.3.3.2. 输入jobs
  输入jobs,可以看到被挂起的hello的pid和它的状态。
  6.3.3.3. 输入pstree -p
  

  可以看到,从先人历程到hello的树为:
  systemd(1)->systemd(985)->gnome-terminal(2427)->bash(2459)->hello(26475)
  
  6.3.3.4. 输入fg
  输入fg后,挂在后台的hello历程被重新调到前台运行,并继续执行。
  

  6.3.3.5. 输入kill
  输入kill -9 26576,发送信号SIGKILL给历程26576,该历程被杀死。
  

  6.3.4. 按ctrl-c
  按下后,会导致段非常,从而内核信号产生SIGINT,发送给shell。shell收到该信号后,向子历程发送SIGKILL来强制终止子历程hello并回收它。这是再运行ps,发现已经没有历程helo,说明它已经被终止且回收。
  

  6.7本章小结

  本章总结了hello历程的执行过程,展示了hello历程的执行以及hello的非常和信号处理。
  

  
7hello的存储管理


  7.1 hello的存储器地点空间

  7.1.1. 逻辑地点
  又称段地点,是段寄存器的偏移地点,是程序汇编后出如今汇编代码中的地点。逻辑地点分为两部门,一个部门为段基址,另一个部门为段偏移量。在CPU保护模式下,需要经过寻址方式的盘算和变更才可以得到内存中的有效地点。
  7.1.2. 线性地点
  是一种中间地点,在地点空间由连续的整数表现,是逻辑地点到物理地点变更之间的中间层。在各段中,逻辑地点是段中的偏移地点,其偏移量加上基地点就是线性地点。
  7.1.3. 假造地点
  是程序访问存储器使用的逻辑地点。使用假造地点时,CPU通过生成一个假造地点来访问主存,这个假造地点在被送至内存钱先转换成适当的物理地点。在linux中,假造地点数值等于线性地点
  7.1.4. 物理地点
  盘算机系统的主存被组织称一个由一个由非常大个连续的字节巨细的单元构成的数组,其中每一个字节都被给与一个唯一的物理地点。
  7.2 Intel逻辑地点到线性地点的变更-段式管理

  逻辑地点表现为[段标识符:段内偏移量],段标识符是一个16位长的字段(段选择符)。
  

  • 根据段选择符的T1选择,当前要转换段位是GDT还是LDT,再根据寄存器,得到其地点。
  • 拿出段选择符的前13位,查找到对应的段形貌符,得到Base。
  • 将基地点和逻辑地点相加,获得线性地点。
  7.3 Hello的线性地点到物理地点的变更-页式管理

  假造地点用VA来表现。处理假造地点即处理线性地点。VA分为假造页号(VPN)与假造页偏移量(VPO)。CPU取出VPN,通过页表基址寄存器来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与假造页偏移量(VPO)结合,得到由物理地点和物理页偏移量(PPO)构成的物理地点。
  7.4 TLB与四级页表支持下的VA到PA的变更

  TLB:处理器向MMU发送假造地点,MMU向TLB发送VPN哀求储存其中的PTE缓存,随后返回PTE条目。若发生缺页,会启动缺页处理程序。
  四级页表:会将VPN分为4段,分别为VPN1、VPN2、VPN3、VPN4,通过树布局对其举行查找,从而找到合适的PTE条目。
  7.5 三级Cache支持下的物理内存访问

  得到物理地点后,根据物理地点从cache中寻找。到了L1后,寻找物理地点检测是否命中,如果不命中则寻找下一级L2,接着L3。如果L3也不命中,则需要从内存中取出相应的块并放入cache中,直到出现命中。
  7.6 hello历程fork时的内存映射

  fork()函数被父历程调用时,内核会创建一个子历程,为新的子历程创建环境,并分配给子历程唯一的pid,且该pid与父历程不同。为了给hello历程创建假造内存,fork()函数创建了当前历程的mm_struct、地区布局和页表的原样副本,并将两个历程的每个页都标记为只读,将两个历程中的地区布局都标记为私有的写时复制。当fork()从新的历程中返回时,hello历程如今的假造内存刚好和调用fork()时的假造内存相同,并且映射的也是同一个物理内存。但其中恣意一个历程举行写操纵时,写时复制机制就会创建新页面,在新的页面中举行写操纵,并且原来的假造内存映射到创建的新页上,因此每个历程都具有私有的地点空间。
  7.7 hello历程execve时的内存映射

  加载并运行hello需要如下几个步骤。
  7.7.1. 删除已存在的用户地区。删除当前历程假造地点的用户部门中的已存在的地区布局。
  7.7.2. 映射私有地区。创建新的代码、数据、堆和栈段。假造地点空间的代码和数据地区被映射为hello文件的.text和.data区。
  7.7.3. 映射共享地区。如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户假造地点空间中的共享地区内。
  7.7.4. 设置程序计数器。设置当前上下文的程序计数器,使之指向代码地区的入口点。
  7.8 缺页故障与缺页中断处理

  当指令引用一个相应的假造地点,而与该地点相应的物理页面不在内存中,会触发缺页非常。缺页非常会调用内核中的缺页非常处理程序,该程序会选择一个牺牲页,即存放在PP3的VP4。若VP4已经被修改了,那么内核就会将它复制回磁盘,否则直接修改。接下来,内存从磁盘复制VP3到内存中的PP3,更新PTE3,然后返回。当非常处理程序返回时,它会重新启动导致缺页的指令,该指令会将导致缺页的假造地点重新发送到地点翻译硬件。此时,VP3已经缓存在主存中,可以正常处理,而不会触发缺页。
  7.9动态存储分配管理

  动态内存分配器维护着一个历程的假造内存地区,称为堆。分配器将堆视为一组不同巨细的块来维护。每个块要么是已分配的,要么是空闲的。
  7.10本章小结

  本章主要先容了hello的存储器地点空间,并先容了逻辑地点、线性地点、假造地点、物理地点等概念。分析了段式管理是怎样完成从逻辑地点到线性地点(假造地点)的变更的。分析了页式管理是怎样完成线性地点到物理地点的变更。分析了TLB与四级页表支持下的VA到PA的变更。先容了三级Cache支持下的物理内存访问的流程。分析了hello历程fork与execve时的内存映射。先容了缺页故障和缺页中断的处理。分析了动态存储分配管理。
  

  
8hello的IO管理


  8.1 Linux的IO装备管理方法

  装备的模型化:文件
  装备管理:unix io接口
  在Linux中,所有的IO装备都被模型化为文件,而所有的输入和输出都被当做相应的文件的读写操纵。将所有的输入和输出都能以一种同一且同等的方式来执行,这边是Linux的IO装备管理方法。
  8.2 简述Unix IO接口及其函数

  8.2.1. Unix IO接口
  Linux将装备映射为文件,所有输入输出都当做文件读写来执行。
  下面是Unix接口的操纵。
  

  • 打开文件
  一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个IO装备。内存返回一个形貌符,用于标识这个文件。内核纪录和这个打开文件相关的所有信息。
  

  • 每个历程开始时三个打开的文件
  Linux shell 创建的每个历程都有3个打开的文件,分别为标准输入、标准输出和标准错误,即stdin、stdout、stderr。
  

  • 改变当前的文件位置
  对于每个打开的文件,内核保持着一个文件位置k,初始值为0。这个文件位置是从文件开始的字节偏移量。应用程序可以或许通过执行seek操纵,显式地设置文件的当前位置k。
  

  • 读写文件
  读操纵即从文件复制n个字节到内存。从当前文件位置k开始读入,然后将k增加到k+n。如果读到文件末端,则触发EOF。同样,写操纵时从内存中复制一些字节到一个文件,从当前文件k开始,然后更新k。
  

  • 关闭文件
  当应用完成了对文件的访问之后,它就会关照内核关闭这个文件。然后内核会释放文件打开时创建的数据布局,并将这个形貌符规复到可用的形貌符池中。无论一个历程因为何种原因终止,内核都会关闭所有打开的文件并释放他们的内存资源。
  8.3 printf的实现分析

  https://www.cnblogs.com/pianist/p/3315801.html
  首先观察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()的第一个参数为const char* 类型的fmt,而后面的参数用...代替。然后我们要设法得知传入参数的个数。
  va_list arg = (va_list)((char*)(&fmt)+ 4);
  这里可以看到arg是一个字符指针,其中(char*)((&fmt)+4)表现的是...中的第一个参数。这是因为C语言中,参数压栈的方向是从右向左的。也就是,先是最右面的参数入栈,然后是倒数第二个。接下来是
  i=vsprintf(buf, fmt, arg);
  首先检察一下vsprintf()的函数体。
  
   int vs_printf(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;
  break;
  case ‘s’:
  break;
  default:
  break;
  }
  }
  
  return  (p-buf);
  

经太过析发现,vsprintf()返回的是要打印的字符串的长度。后面一句
write(buf, i);
这句是写操纵,将buf与参数数目i传入,将buf中的i个元素写到终端。然后我们观察一下write函数的实现。
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
Int INT_VECTOR_SYS_CALL
接下来看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
在这里,sys_call实现了显示字符串。
8.4 getchar的实现分析

可以看到,getchar的实现是用一个宏实现的
#define getchar() getc(stdin)
getchar()有一个int类型的返回值。当程序调用getchar()时,程序等着用户按键。用户输入的字符被放在键盘缓冲区中,直到用户按下回车为止。当用户键入回车后,getchar()开始从stdin中每次读入一个字符。如果用户在按下回车之前输入了不止一个字符,那么其他字符会保存在键盘缓冲区中,等待后续的函数调用读取。
8.5本章小结

本章先容了hello的IO管理方法,分析了printf()和getchar()函数的实现

结论


一个简朴程序的执行,需要软件和硬件、软件与操纵系统等的交互和共同。外貌上简朴,但现实上非常复杂。
附件

hello.i 预处理器处理后得到的文件。
hello.s 编译器处理后得到的文件,将高级语言翻译为汇编
hello.o 汇编器处理后得到的可重定位目标文件
hello   链接器链接后得到的可执行文件
hello_o_elf.txt 由hello.o得到的.elf文件
hello1_elf.txt 由hello得到的.elf文件
hello_o_s.txt 由hello.o得到的反汇编文件
hello_s.txt 由hello得到的反汇编文件
script.sh 用于生成hello所使用的脚本文件


参考文献


为完成本次大作业你翻阅的书籍与网站等


  •  【CSAPP】程序人生-Hello’s P2P - 知乎 (zhihu.com)
  • 程序人生-Hello’s P2P-CSDN博客



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

东湖之滨

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表