哈工大CSAPP大作业

第1章 概述

1.1 Hello简介

hello的源码hello.c文件,要生成可执行文件,首先要进行预处理,其次要进行编译生成汇编代码,接着进行汇编处理生成目标文件,目标文件通过链接器形成一个可执行文件,可执行文件需要一个执行环境,它可以在linux下通过shell进行运行,与计算机其他经常文件同步运行,并通过异常处理机制相应信号。在运行的过程中,程序通过Intel内存管理机制一步步访问逻辑地址、虚拟地址、物理地址,从而进行数据交换,还可以通过IO机制进行输入输出交互

1.2 环境与工具

环境:ubuntu64位 vmware虚拟机环境中运行

gdb

edb

ida pro

visual studio

hexedit

notepad++

objdump

1.3 中间结果

hello.c源程序

hello.i预处理后的源程序

hello.s汇编代码

hello.o目标程序

hello可执行文件

elfo.txt链接前的elf文件信息

elfe.txt链接后的elf文件信息

asmo.txt hello.o反编译结果

asme.txt hello反编译结果

1.4 本章小结

题太多了,太累了QAQ

 

(第1章0.5分)

 

 


第2章 预处理

2.1 预处理的概念与作用

概念:在编译之前进行的处理。

作用:1.宏定义2.文件包含3.条件编译

 

2.2在Ubuntu下预处理的命令

gcc hello.c -E

 

 

2.3 Hello的预处理结果解析

hello.c程序中只包含三条预处理指令

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

作用是文件包含,即包含stdio.h,unistd.h,stdlib.h三个文件

通过-E指令,只激活预处理指令,可以看到执行完这三条文件包含指令的结果:将三个文件的内容引入,代码量提升至约800行(详见hello.i)

 

2.4 本章小结

预处理命令,可以在编译器编译之前,提前进行一些操作,比如定义常量,还可以进行条件编译以方便调试,可以进行文件引入来导入一些预先写好的模块,便于程序的组织和调试和一些特殊的编程技巧的实现,是一项非常有用的功能。

 

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

 

编译(compilation , compile)

利用编译程序从源语言编写的源程序产生目标程序的过程。

作用:

把用高级程序设计语言书写的源程序,翻译成等价的计算机汇编语言

 

3.2 在Ubuntu下编译的命令

gcc hello.i -S

 

3.3 Hello的编译结果解析

1.int sleepsecs = 2.5

该句定义一个int类型全局变量sleepsecs,其值为2(向整数向下取整后的结果),在伪汇编文件中,先定义一个全局符号sleepsecs,用于标识和连接。

       .globl     sleepsecs

在.data指令后,描述一些该变量的详细信息

       .type      sleepsecs, @object 将sleepsecs定义为对象类型

       .size       sleepsecs, 4 占用4个字节

sleepsecs: 定义sleepsecs对应的标签

       .long      2 定义为长整型数2

      

2.main函数

       .text 在text节中定义

       .globl     main 声明全局符号global

       .type      main, @function 将main声明为函数

随后定义main:标签,后跟main函数伪汇编指令

 

3.int i

这是一个未初始化的局部变量,它存放在栈中,位置是-4(%rbp)

4.if(argc!=3)

       cmpl      $3, -20(%rbp)

       je    .L2

 

argc作为main的参数,存放在-20(%rbp)的位置,将它与3比较,如果相等就不执行后面大括号的部分

5.printf("Usage: Hello 学号 姓名!\n");

       movl      $.LC0, %edi

       call puts

将.LC0的部分传入参数,执行printf

.LC0定义在.rodata节中,是一个字符常量

.LC0:

       .string       "Usage:Hello\345\255\246\345\217\267\345\247\223\345\220\215\357\274\201"

6.exit(1);

       movl      $1, %edi

       call exit

参数为1,执行exit函数

7.for(i=0;i<10;i++)

.L2:

       movl      $0, -4(%rbp)

       jmp .L3

该节初始化i为0跳转到判断节

.L3:

       cmpl      $9, -4(%rbp)

       jle   .L4

如果满足条件就跳转、继续执行

       addl $1, -4(%rbp)

循环变量递增

8. printf("Hello %s %s\n",argv[1],argv[2]);

       movq     -32(%rbp), %rax

       addq      $16, %rax

       movq     (%rax), %rdx

将argv[2]传给%rdx

       movq     -32(%rbp), %rax

       addq      $8, %rax

       movq     (%rax), %rax

       movq     %rax, %rsi

将argv[1]传给%rsi

       movl      $.LC1, %edi

将字符串常量"Hello %s %s\n"传给%edi

       movl      $0, %eax

将%eax赋0

       call printf

执行printf

以下格式自行编排,编辑时删除

9.sleep(sleepsecs);

       movl      sleepsecs(%rip), %eax

       movl      %eax, %edi

       call sleep

将sleep传给%edi作为参数,执行sleep函数

10. getchar()

       call getchar

执行getchar函数

 

11. return 0;

leave

释放堆栈空间

       ret

返回

3.4 本章小结

本节涉及到的指令全部为gun汇编程序(gas)的伪汇编指令,相比最后的汇编指令内容更为精简,方便阅读、分析。程序将常量放入.rodata节,初始化全局变量放入.data节,通过标签定义和跳转等方式定义许多操作,为后序的汇编和链接生成可执行文件准备。

(第32分)


第4章 汇编

4.1 汇编的概念与作用

概念:

把汇编语言翻译成机器语言的过程称为汇编

 

作用:

将汇编语言翻译成机器语言

 

4.2 在Ubuntu下汇编的命令

       gcc -c hello.s

 

 

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

 

 

各Section基本信息:

 

Name:名称

Type:类型

Address:地址

Offset:地址偏移量

Size:大小

EntSize:全体大小

Flag:旗标

Link:被重定位的符号所在的符号表的section index

Info:需要被重定位的section的index

Align:对齐信息

包含重定位信息的节

 

offset表示该符号在被重定位的section中的偏移

info的高4个字节表示该符号在.symtab中的index,低4字节表示重定位的类型

type表示符号类型

sym.value表示链接过程中将要写入地址的位置

sym.name表示符号名称

append追加地址

 

4.4 Hello.o的结果解析

机器语言是直接用二进制代码指令表达的计算机语言,指令是用0和1组成的一串代码,它们有一定的位数,并分成若干段,各段的编码表示不同的含义。

每一条汇编语句被映射为若干二进制指令码,将机器语言的每一条指令符号化:指令码代之以记忆符号,地址码代之以符号地址。

在汇编语言中,操作数用十进制表示,而在机器语言中,用十六进制表示,如hello.o中:

机器语言中命令

   4:    48 83 ec 20             sub    $0x20,%rsp

在汇编语言hello.s中对应为

      subq      $32, %rsp

 

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

在汇编语言中,分支跳转、函数调用,使用标签定位位置,而在机器语言中使用地址+偏移量计算要跳转到的实际地址。

如hello.o中:

  6f:    7e c1                jle    32 <main+0x32>

  71:    e8 00 00 00 00           callq  76 <main+0x76>

在hello.s中对应为:

      jle   .L4

      call getchar

4.5 本章小结

汇编是将计算机不能读懂的汇编语言翻译成计算机能读懂的机器语言的不可缺少的重要步骤.

(第41分)

_
第5章 链接

5.1 链接的概念与作用

概念:

链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。

作用:

链接在软件开发中扮演着一个关键的角色,因为它使分离编译成为可能。

5.2 在Ubuntu下链接的命令

ld -o hello -dynamic-linker /lib/ld-linker.so.2 /usr/lib/crt1.o /usr/lib/crti.o -l hello.o /usr/lib/ctrn.o

 

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

 

 

offset:偏移量

virtaddr:虚拟地址

phyaddr:物理地址

filesiz:文件中的大小

memsiz:内存中的大小

flags:旗标

align:对齐

 

5.4 hello的虚拟地址空间

   

 

实际运行中,所有虚拟地址空间段大小都为0x1000,且一段中包含一个或多个5.3中的程序段。动态链接库中的文件映射到内存的内容,与hello文件中映射到内存的内容地址间隔较大。程序中还包括[stack],[vvar],[vdso],[vsyscall]等特殊用途的地址段。

5.5 链接的重定位过程分析

 

 

不同:

hello中包含一些外部文件的宏定义、变量、库函数和操作系统的启动代码等,且.o文件.text节从0开始,而可执行文件.text节并非从0开始。

过程:分为符号解析和重定位两步

  1. 符号解析:目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量。符号解析的目的是将每个符号引用正好和一个符号定义关联起来
  2. 重定位:编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。

重定位:

hello.o的文件中包含一些重定位条目

 

这些重定位条目告诉链接器32位PC相对地址或32位绝对地址进行重定位,这些重定位条目通过计算地址或直接调用保存的绝对地址,达到重定位的目的。

无论何时汇编器遇到对最终位置未知的目标引用,会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的条目放在.rel.data中

             

5.6 hello的执行流程

_start

__libc_start_main

__GI___cxa_atexit

__internal_atexit

__GI___cxa_atexit

__internal_atexit

__new_exitfn

__internal_atexit

__GI___cxa_atexit

__libc_start_main

_setjmp

__sigsetjmp

__sigjmp_save

__libc_start_main

__GI_exit

__run_exit_handlers

__GI___call_tls_dtors

__run_exit_handlers

__do_global_dtors_aux

deregister_tm_clones

__do_global_dtors_aux

_fini

__run_exit_handlers

_IO_cleanup

_IO_unbuffer_all

_IO_cleanup

_IO_flush_all_lockp

_IO_cleanup

_IO_unbuffer_all

_IO_cleanup

_IO_unbuffer_all

_IO_new_file_setbuf

_IO_default_setbuf

_IO_new_file_sync

_IO_default_setbuf

__GI__IO_setb

_IO_default_setbuf

__GI__IO_setb

_IO_default_setbuf

_IO_new_file_setbuf

_IO_unbuffer_all

_IO_new_file_setbuf

_IO_default_setbuf

_IO_new_file_sync

_IO_default_setbuf

__GI__IO_setb

_IO_default_setbuf

__GI__IO_setb

_IO_default_setbuf

_IO_new_file_setbuf

_IO_unbuffer_all

_IO_cleanup

__run_exit_handlers

__GI__exit

5.7 Hello的动态链接分析

动态链接项目如下图(主要为两个.so文件相关内容)

  

 

分析GOT的变化

dl_init前:

 

dl_init后

 

0x6010208~0x601020d字节发生了变化

5.8 本章小结

(个人认为,在全书中,“链接”这一章的难度比所有其他章节之和还要大,搞清楚动态链接的全过程十分困难,课本上也有意的跳过了一些部分,导致一些细节问题十分费解。)

链接是组建大型程序和团队编程不可缺少的重要部分,掌握链接器的一些原理和动态链接是非常有必要的,也是学习库打桩等强大机制的基础。虽然hello.c很简单,但是也需要和标准库进行链接。了解hello.c链接的来龙去脉,对掌握链接技术很有帮助。

(第51分)

 


第6章 hello进程管理

6.1 进程的概念与作用

概念:

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

作用:

由于程序是静态的,我们看到的程序是存储在存储介质上的,它无法反映出程序执行过程中的动态特性,而且程序在执行过程中是不断申请资源,程序作为共享资源的基本单位是不合适的,所以需要引入一个概念,它能描述程序的执行过程而且可以作为共享资源的基本单位,这个概念就是进程。进程解决了系统资源调度等一系列问题。

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

Shell俗称壳(用来区别于内核),是指“提供使用者使用界面”的软件,就是一个命令行解释器。

bash 是一个为GNU项目编写的Unix shell,也就是linux用的shell。

所以Shell-bash的“作用”是linux系统的一个命令行解释器

 

 

 

 

流程:

 

 

6.3 Hello的fork进程创建过程

shell调用fork函数,形成自身的一个拷贝(子进程),为运行hello做准备

6.4 Hello的execve过程

在shell的子进程中执行execve函数,将参数传给Hello程序,并执行Hello

6.5 Hello的进程执行

一开始,Hello运行在用户模式,当程序收到一个信号时,进入内核模式,运行信号处理程序,之后再返回用户模式。在Hello运行的过程中,cpu不断切换上下文,使Hello程序运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。

6.6 hello的异常与信号处理

1.输入Ctrl-C时,程序终止

 

 

处理过程是向程序发送SIGINT信号,程序执行默认行为:停止执行

 

2.输入Ctrl-C时,程序挂起

      

 

处理过程是向程序发送SIGSTP信号,程序执行默认行为:挂起程序,之后会返回shell中

 

  1. 乱按+回车

 

 

输入的内容会被留在缓冲区中,当hello执行结束,返回shell中,shell会从缓冲区读取并尝试解析这些内容

  1. ps jobs pstree fg kill命令

 

 

ps:显示当前进程的状态

jobs:查看后台运行的进程

fg:恢复一个后台进程

pstree:显示进程树

kill:结束一个进程

 

可能会产生IO中断、时钟中断、系统调用等等,会产生SIGINT、SIGSTP等信号。

6.7本章小结

linux命令行shell是一个非常强大的工具,用它可以更方便的执行Hello和发送各种命令请求。通过信号等方式可以实现异常处理,让Hello在顺序执行者也能处理一些突发状况和实现一些功能。进程调度实现了各个进程计算资源合理分配,互不干扰,提高了系统稳定性和效率。

(第61分)


第7章 hello的存储管理

7.1 hello的存储器地址空间

物理地址(physical address)

用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。

 

逻辑地址(logical address)

逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。如Hello中sleepsecs这个操作数的地址。

 

线性地址(linear address)或也叫虚拟地址(virtual address)

跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。

 

7.2 Intel逻辑地址到线性地址的变换-段式管理

  段式内存管理方式就是直接将逻辑地址转换成物理地址,也就是CPU不支持分页机制。其地址的基本组成方式是段号+段内偏移地址。

在x86保护模式下,段的信息(段基线性地址、长度、权限等)即段描述符占8个字节,段信息无法直接存放在段寄存器中(段寄存器只有2字节)。Intel的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值(index)。

首先给定一个完整的逻辑地址[段选择符:段内偏移地址],

      1.看段选择描述符中的T1字段是0还是1,可以知道当前要转换的是GDT中的段,还是LDT中的段,再根据指定的相应的寄存器,得到其地址和大小,我们就有了一个数组了。

      2.拿出段选择符中的前13位,可以在这个数组中查找到对应的段描述符,这样就有了Base,即基地址就知道了。

      3.把基地址Base+Offset,就是要转换的下一个阶段的地址。

7.3 Hello的线性地址到物理地址的变换-页式管理

分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页(page),每页包含4k字节的地址空间(为简化分析,我们不考虑扩展分页的情况)。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表(page table)。

       为了节约页表占用的内存空间,x86将线性地址通过页目录表和页表两级查找转换成物理地址。32位的线性地址被分成3个部分:最高10位 Directory 页目录表偏移量,中间10位 Table是页表偏移量,最低12位Offset是物理页内的字节偏移量。页目录表的大小为4k(刚好是一个页的大小),包含1024项,每个项4字节(32位),项目里存储的内容就是页表的物理地址。如果页目录表中的页表尚未分配,则物理地址填0。页表的大小也是4k,同样包含1024项,每个项4字节,内容为最终物理页的物理内存起始地址。

      

       每个活动的任务,必须要先分配给它一个页目录表,并把页目录表的物理地址存入cr3寄存器。页表可以提前分配好,也可以在用到的时候再分配。

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

 

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

先访问一级缓存,不命中时访问二级缓存,再不命中访问三级缓存,再不命中访问主存,如果主存缺页则访问硬盘

 

7.6 hello进程fork时的内存映射

执行新进程(hello)时,为这个新进程创建虚拟内存

  1. 创建当前进程的的mm_struct, vm_area_struct和页表的原样副本
  2. 两个进程中的每个页面都标记为只读
  3. 两个进程中的每个区域结构(vm_area_struct) 都标记为私有的写时复制

在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存, 随后的写操作通过写时复制机制创建新页面

7.7 hello进程execve时的内存映射

 

  1. 删除已存在的用户区域
  2. 创建新的区域结构: 代码和初始化数据映射到.text和.data区(目标文件提供), .bss和栈映射到匿名文件
  3. 设置PC,指向代码区域的入口点

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

以下格式自行编排,编辑时删除

缺页故障:需要访问的页不在主存,需要操作系统将其调入后才能访问。

有三种情况:

 

只有正常缺页时,系统才会调入需要访问的页,并再次执行访问该页的命令。

7.9动态存储分配管理

基本方法:维护一个虚拟内存区域“堆”,将堆视为一组不同大小的 块(blocks)的集合来维护,每个块要么是已分配的,要么是空闲的,需要时选择一个合适的内存块进行分配。

  1. 记录空闲块,可以选择隐式空闲链表,显示空闲链表,分离的空闲链表和按块大小排序建立平衡树
  2. 放置策略,可以选择首次适配,下一次适配,最佳适配
  3. 合并策略,可以选择立即合并,延迟合并
  4. 需要考虑分割空闲块的时机,对内部碎片的忍耐阈值.

7.10本章小结

通过高速缓存、虚拟内存、动态内存分配,可以实现快速、高校、利用率高的储存空间管理。可以通过内存映射等方式实现文件共享。储存管理是一个相当重要、值得研究的机制。

(第7 2分)


第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:将设备抽象成文件

设备管理:通过unix io接口管理

8.2 简述Unix IO接口及其函数

打开和关闭文件:open()and close()
读写文件:read() and write()
改变当前的文件位置 lseek()

 8.3 printf的实现分析

1.从vsprintf生成显示信息到write系统函数,到陷阱-系统调用 int 0x80或syscall.

2.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

3.显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

1.异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

2.getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

输入输出看似简单,实际是一个非常精巧的过程,从程序发出请求到系统函数调用到设备相应,需要执行许多步骤,往往也是拖慢程序的主要因素和一些崩溃异常的高发地,需要谨慎选用函数、命令实现目的。

(第81分)

结论

hello的源码hello.c文件,要生成可执行文件,首先要进行预处理,其次要进行编译生成汇编代码,接着进行汇编处理生成目标文件,目标文件通过链接器形成一个可执行文件,可执行文件需要一个执行环境,它可以在linux下通过shell进行运行,与计算机其他经常文件同步运行,并通过异常处理机制相应信号。在运行的过程中,程序通过Intel内存管理机制一步步访问逻辑地址、虚拟地址、物理地址,从而进行数据交换,还可以通过IO机制进行输入输出交互。

通过学习ics这门课程,深感计算机这个庞大体系的复杂、精巧,从电路到电路组合,再到硬件集成、软件调配,每一处都井井有条、随处显现着前人的智慧。不少复杂概念的学习,通过这门课感觉仅仅只是入了个门,距离熟练运用甚至涉及差距仍然较大,但不妨碍进行一些思维上的创新。

目前的电子计算机建立在二进制基础上,将来基础物理突破之后,可能会实现三进制、四进制的计算机,没准可以实现质的飞越。或许神经科学有了巨大飞越,让“电脑”真正构建起一个类似于人脑的神经结构,实现一个强的人工智能。

参考文献

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

 [1]《深入理解计算机系统》

 [2] 哈工大计算机系统课程讲课ppt

 [3] [转]printf 函数实现的深入剖析https://www.cnblogs.com/pianist/p/3315801.html

 [4] 【不周山之读薄 CSAPP】肆 链接 https://wdxtub.com/2016/04/16/thin-csapp-4/

 [5] Linux下库函数动态链接过程分析-结合glibc-2.11源码 https://blog.csdn.net/lzshlzsh/article/details/6066628

 [6] Linux下 可视化 反汇编工具 EDB 基本操作知识 https://blog.csdn.net/hahalidaxin/article/details/84442132

 [7] gdb基本命令(非常详细)

https://blog.csdn.net/z15818264727/article/details/69668820

[8] Linux中查看进程的虚拟地址空间内存布局 https://blog.csdn.net/ASJBFJSB/article/details/81429576

[9] GNU ARM 汇编伪指令(Assembler Directives) https://blog.csdn.net/liuzq/article/details/83615085

[10] GCC常用参数详解 https://www.cnblogs.com/zhangsir6/articles/2956798.html

转载于:https://www.cnblogs.com/hyfer/p/10188436.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值