深入理解计算机系统(csapp)阅读笔记——第三章程序机器级表示

1.历史观点

  • Intel处理器俗称X86,下面举例了一下Inter处理器的模型,以及它们的相关特性:
    • 8086(1978年,29K个晶体管):第一代单芯片、16位微处理器之一。地址只有20位长。
    • 80286(1982年,134K个晶体管):增加了更多的寻址模式。这种计算机是MS Windows最初的使用平台
    • i386(1985,267K个晶体管):将体系结构扩展到32位,增加了平坦寻址模式这是Intel系列中第一台全面支持Unix操作系统的机器
    • i486(1989,1.2M个晶体管):改善了性能
    • Pentium(1993,3.1N个晶体管):改善了性能
    • PentiumPro(1995,5.5N个晶体管):引入全新的处理器设计,在内部被称为P6微体系结构
    • Pentium/MMX(1997,4.5N个晶体管):增加了一类新的处理整数向量的指令。每个数据大小可以使1/2或4字节。每个向量总长64位。
    • Pentium II/III/4/4:扩展了指令
    • Pentium 4E(2004,125M个晶体管):增加了超线程。这种技术可以在一个处理器上同时运行两个程序
    • Core 2:回归到类似于P6的微体系结构。Intel的第一个多核微处理器。即多个处理器实现在一个芯片上,但不支持超线程
    • core i7:及支持超线程,又有多核。
  • 摩尔定律:芯片上的晶体管每年都会翻一番
    在这里插入图片描述

2.程序编码

(1)机器级代码

  • 对于机器级编程来说,其中两种抽象尤为重要:
    • 有指令集体系结构或指令集架构(Instruction Set Architecture,ISA)来定义机器及程序的格式和行为:它定义了处理器状态、指令的格式、以及每条指令对状态的影响。
    • 机器级程序使用的内存的地址是虚拟地址,提供的内存模型看上去是一个非常大的字节数组,存储器的系统实现是将多个硬件存储器和操作系统软件组合起来。
  • x86-54的机器代码隐藏的处理器状态:
    • 程序计数器(通常称为“PC”,在x86-64中用%rip表示)
    • 整数寄存器:文件包含16个命名的位置,分别存储64位的值。这些寄存器可以存储地址或整数数据。
    • 条件码寄存器:保存着最近执行的算数或逻辑指令的状态信息,它们用来实现控制或数流中的条件变化
    • 一组向量寄存器:可以存放一个或多个整数或浮点数值。
  • X86-64的虚拟地址是由64位的字来表示的。在目前的实现中,这些地址的高16位必须设置为0,所以一个地址实际上能够指定的是248范围内的一个字节。
  • gcc的使用:
    • 生成二进制文件.o:gcc -Og -c name.c
    • 反汇编二进制文件:objdump -d mstore.o
    • 生成汇编文件.s:gcc -Og -S (-masm=Intel) name.c

(2)数据格式

  • x86-64中指针存储为8字节的四字。
  • C语言数据类型对应的大小
    在这里插入图片描述

(3)访问信息

  • 一个x86-64的CPU包含一组16个存储64位值的通用目的寄存器。他们的名字都以%r开头:
    在这里插入图片描述
  • 规则:当这些指令以寄存器作为目标时,对于生成小于8字节结果的指令,寄存器中剩下的字节会怎么样,对此有两条规则:生成1字节和2字节数字的指令会保持剩下的字节不变;生成4字节数字的指令会把高位4字节置0
  • 操作数指示符:
    在这里插入图片描述
  • 普通数字传送指令:
    在这里插入图片描述
    传输指令的两个操作数不能都指向内存位置。
    寄存器部分的大小必须与指令最后一个字符指定的大小匹配。
    64位汇编中mov指令第一个操作符是源操作符,第二个操作符是目的操作符。32位和16位则相反。
    movq和movabsq:常规的movq指令只能以表示32位补码数字的立即数作为源操作数,然后把这个值符号扩展得到64为的值,放到目的位置。movabsq指令能够以任意64位立即数值作为源操作数,并且只能以寄存器作为目的。
  • 零扩展传送指令:
    在这里插入图片描述
  • 字符扩展传送指令:
    在这里插入图片描述
    扩展一般是从存储器取出的值扩展后送入目的存储器中
  • 练习1: 一些有错误的代码:
    在这里插入图片描述
  • 练习2:如图:
    在这里插入图片描述
void decode1(long *xp,long *yp,long *zp)
{
	long temp1 = *xp;
	long temp2 = *yp;
	long temp3 = *zp;
	*yp = temp1;
	*zp = temp2;
	*xp = temp3;
}
  • 压入和弹出栈数据
    在这里插入图片描述
    在这里插入图片描述
    每次出栈%esp加8,进栈减8
    出栈后原有的内存依然没有改变,直到被覆盖

3.算数和逻辑操作

  • 基本算数操作:除了LEA其他指令都有d,w,l,q四个变种:
    在这里插入图片描述
  • leaq加载有效地址的妙用:
    leaq第一个操作数看上去是一个内存引用,但是该指令并不是从指定的位置读取数据,而是将有效地址写入到目的操作数。
    使用leaq指令执行家法和有限形式的乘法
long scale(long x,long y,long z)
{
	long t = x + 4*y +12*z;
	return t;
}

//使用lea实现
//long scale(long x,long y,long z);
//x in %rdi,y in %rsi,z in %rdx
//使用比例寻址注意比例要是2^n
scale:
	leaq (%rdi,%rsi,4),%rax  //x+4*y
	leaq (%rdx,%rdx,2),%rdx  //z+2*z
	leaq (%rax,%rdx,4),%rax  
	ret
  • 移位操作
    移位量可以是一个立即数,或者放在单字节寄存器%cl中(不能放到别的寄存器)
    虽然移位量的编码范围达到28-1=255,但是左移的位数要小于等于需要左移的数据类型的长度,所以当移位量为0xFF时,指令salb会移7位,salw会移15位,sall会移31位,而salq会移63位。
    练习题3:如图:
    在这里插入图片描述
    在这里插入图片描述
  • 特殊的算数操作
    在这里插入图片描述
    除法指令的值存储在%rdx中
    乘法imulq有两种形式,一种就是双操作数形式,实现两个最多64位乘法结果截断为64位,此时由于无符号数和有符号数的位级等效性是等效的,所以可以用于无符号数和有符号数
    第二种就是现在的单操作数形式,高位存在%rdx,低位存在%rax。
    除法当被除数是128位时,除后商存在%rax,而余数存在%rdx
    **当被除数为64位的时候,%rdx应该存放被除数的符号位(有符号运算)或者全0(无符号运算)**这个操作可以使用指令cqto来完成。这条指令不需要操作数——它隐含独处%rax的符号位,并将它复制到%rdx的所有位

4.控制

  • 一组单个位条件码寄存器中常见的条件码
    在这里插入图片描述
  • 两个和条件码配合使用的指令
    在这里插入图片描述
    TEST的典型用法是:
    (1)两个操作数一样,用来判断该操作数是负数还是正数还是0
    (2)其中一个操作数是一个掩码,用来指示哪些位应该被测试
  • 条件码常用的使用方法:
    • 根据条件码的某种组合,将一个字节设置为0或者1
    • 可以条件跳转到程序的某个其他的部分
    • 可以有条件地传送数据。
  • 使用SET指令实现用法1:
    在这里插入图片描述
    SET指令的目的操作数是低位单字节寄存器元素之一,或是一个字节的内存位置,指令会将这个字节设置成0或者1
    使用实例:
//实现比较无符号数的a,b,a<b返回1,a>b返回0
int comp(unsigned long a,unsigned long b)
//a in %rdi , b in %rsi
{
	cmpq %rsi,%rdi  //注意顺序,后面一个在前
	setl %al  //得到结果存于al
	movzbl %al,%eax  //扩展成32位int型返回,注意这个时候高4位也清零了
	ret
}  

在判断a-b的时候:
有符号数:当SF=1且OF=0或SF=0且OF=1(负溢出)时a<b,即SF^OF=1时a<b
使用SF、OF和ZF判断
无符号数:当a-b>0时产生进位CF=1,所以使用CF和ZF判断就行
练习4:如图:
在这里插入图片描述
在这里插入图片描述

  • 使用JMP指令实现用法2(条件控制):
    在这里插入图片描述
    实例:
    在这里插入图片描述
    从上图可以看出,第一个跳转指令的目标指令编码是0x03(偏移量),在执行这条指令的时候PC指向的是0x05,执行指令后PC=0x03+0x05 = 0x08,于是跳转到0x8
    第二个跳转指令的目标指令编码是0xf8(-8),在执行这条指令的时候PC指向的是0x13,执行指令后PC=0x13+0xf8 = 0x05,于是跳转到0x5。
    由此可知机器码的编码是相对编码而不是绝对编码,因为链接后绝对地址会变动,也是节省了字节。
    练习5:如图:
    在这里插入图片描述
    A:
void cond(long a,long *p)
{
	if(!p)
		return;
	if(*p>=a)
		return;
	*p = a;
	return;
}

B:
对于&&和||,编译器会做如下优化:
&&:编译器会从左到右逐个判断,如果出现有为0的选项就直接跳出判断不做剩下的判断
|| :编译器会从左到右逐个判断,如果出现有为1的选项就直接跳出判断不做剩下的判断

  • 使用cmov指令实现用法2(条件传送)(慎用):
    在这里插入图片描述
    不能使用的情况:
long cread(long *xp)
{
	return (xp? *xp:0);
}

如果上述编译器使用条件传送语句,会导致间接引用空指针的错误

  • 使用跳转表编译Switch语句
    (1)C语言
    在这里插入图片描述
    (2)汇编代码
    在这里插入图片描述
    由图可见,维护了一个跳转表,是一个一维数组,其中存储了每个跳转的地址。第五行是访问方法。其中JMP后面带着*说明是间接跳转
    在汇编代码中,跳转表用以下声明表示
    在这里插入图片描述

5.过程

  • 过程是软件中一种很重要的抽象。它提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能。
  • 一个过程必须包括以下机制:
    • 转移控制:程序计数器的控制
    • 传递数据:传递的参数
    • 分配和释放内存:为局部变量分配和释放空间
  • 转移控制——CALLandRET
    在这里插入图片描述
  • 数据传送
    x86-64中,可以通过寄存器最多传递6个整型参数。
    通过栈传递参数时,所有的数据大小都向8的倍数对齐。
  • 到目前为止我们看到的大多数过程示例都不需要超出寄存器大小的本地存储区域,不过有些时候,局部数据必须存放在内存中,常见的情况包括:
    • 寄存器不足够存放所有的本地数据
    • 对一个局部变量使用地址运算符“&”,因此必须能够为它产生一个地址
    • 某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到
  • 寄存器中的局部存储空间:寄存器是唯一被所有过程共享的资源,虽然在给定时刻只有一个过程是活动的,我们仍然必须确保党一个过程(调用者)调用另一个过程(被调用者)时,被调用者不会覆盖调用者稍后会使用的寄存器值。
    (1)寄存器%rbx、%rbp和%12~%r15被划分为被调用者保存寄存器。当过程P调用过程Q时,Q必须保存这些寄存器的值,保证它们的值在Q返回到P时与Q被调用时是一样的。在Q中要么根本不变,要么先压入栈中,最后再出栈
    (2)所有其他的寄存器,除了栈指针%rsp,都分类为调用者保存寄存器意味着任何函数都能修改它们。可以这样理解:过程P在某个此类寄存器中有局部数据,然后调用过程Q。因为Q可以随意修改这个寄存器,所以在调用之前首先保存好这个数据时P的责任
  • 递归过程:
    (1)C语言
    在这里插入图片描述
    (2)汇编代码
    在这里插入图片描述

6.数组分配和访问

  • 嵌套的数组:嵌套的数组是使用“行优先”的方式存储的,因为内存空间是一维的
    在这里插入图片描述
    数组元素的内存地址计算方法:
    在这里插入图片描述

7.异质的数据结构

  • 结构体是连续存储的
    在这里插入图片描述
  • union提供了中方式,能够规避C语言的类型系统,允许以多种类型来引用一个对象。它们是用不同的字段来引用相同的内存块。
    在这里插入图片描述
    在这里插入图片描述
    注意结构体中存在字节对齐,方便寻址

8.在机器级程序中将控制与数据结合起来

  • 使用指针操作函数:
    在这里插入图片描述
    在这里插入图片描述

9.内存越界引用和缓冲区溢出

  • 缓冲区溢出:
    C语言:
    在这里插入图片描述
    汇编语言
    在这里插入图片描述
    由汇编语言可知,系统为该函数分配了24字节的空间,其中9字节(最后一个字节是null结束符)用于存储字符串,另外的15字节是未被使用的栈空间,一旦用户输入的字符串长度超过24字节,就会破坏状态,要么就是将返回地址破坏,导致不知道返回到哪里去了,要么就是破坏上一级调用所保持的状态
    缓冲区溢出更加致命的使用:
    在这里插入图片描述
  • 对抗缓冲区溢出攻击:
    (1)栈随机化
    在过去,程序的栈地址非常容易预测。对于所有运行同样程序和操作系统版本的系统来说,在不同机器之间,栈的位置是相当固定的。因此,攻击者很容易攻击到很多机器,这种现象常被称作安全单一化。
    栈随机化的思想使得栈的位置在程序每次运行时都有变化。因此,即使许多机器都运行同样的代码,它们的栈地址都是不同的。实现方式是:程序开始时,在栈上分配一段0~n字节之间的随机大小的空间
    地址空间布局随机化ASLR(Address-Space Layout Randomzation),每次运行时程序的不同部分,包括程序代码、库代码、栈、全局变量和堆数据,都会被加载到内存的不同区域。
    空操作雪橇:
    在这里插入图片描述
    (2)栈破坏检测
    栈保护者机制:检测缓冲区越界。其思想是在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀值。在恢复寄存器状态和从函数返回之前,程序检查金丝雀值是否被该函数的某个操作或者该函数调用的某个函数的某个操作改变了。如果是的,那么程序异常终止。
    (3)限制可执行代码区域
    消除攻击者向系统中插入可执行代码的能力。一种方法是限制哪些内存区域能够存放可执行代码(只有保存编译器产生的那部分的代码内存才需要是可执行的)
  • 管理变长指针,使用%rbp
    在这里插入图片描述
    注意%rbp是被调用者保护寄存器,在调用前必须保存之前的值。

10.浮点代码

  • 处理器的浮点体系结构:
    • 如何存储和访问浮点数值。通常是通过某个寄存器方式来完成
    • 对浮点数据操作的指令
    • 向函数传递浮点数参数和从函数返回浮点数结果的规则
    • 函数调用过程中保存寄存器的规则
  • AVX浮点体系结构允许数据存储在16个YMM寄存器中,它们的名字为%ymm0~%ymm15。每个YMM寄存器都是256位(32字节)。当对标量数据操作时,这些寄存器只保存浮点数,而且只使用低32或64位。汇编代码用寄存器的SSE XMM寄存器名字%xmm0-%xmm15俩引用它们。
    在这里插入图片描述
  • 浮点传送和转换操作
    在这里插入图片描述
    在两个寄存器之间传送数据,绝不会出现错误对齐的状况
  • 整数和浮点数之间的转换
    在这里插入图片描述
    在这里插入图片描述
    整数转换为浮点数使用三操作数格式,第二个操作数和目的操作数是一样的。
  • 单精度和双精度的转换
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 浮点运算操作
    在这里插入图片描述
    注意:第一个源操作数可以是一个XMM寄存器或一个内存位置,第二个源操作数和目的操作数必须是XMM寄存器。
  • 定义和使用浮点常数
    AVX浮点操作不能以立即数值作为操作数。编译器必须为所有的常量值分配和初始化存储空间。
    在这里插入图片描述
  • 在浮点代码中使用位级操作
    在这里插入图片描述
  • 浮点比较操作
    在这里插入图片描述
    在这里插入图片描述
发布了33 篇原创文章 · 获赞 3 · 访问量 618

猜你喜欢

转载自blog.csdn.net/qq_43647628/article/details/104557485
今日推荐