CSAPP-第3章 程序的机器级表示

版权声明:转载请说明出处 https://blog.csdn.net/coding__cat/article/details/86528380

写在最前面
学完这一章你即将成为一个汇编级程序猿……想想就很激动٩(๑>◡<๑)۶
这也就意味着……这章很重要,内容也很多,这篇博文也贼长……
还是和上一章的那句话一样,有什么不明白的,最好写写代码跑一跑……关于栈的东西,不如画个图……
当然,为了csapp之后的学习……你需要一个linux系统……

本章内容

  • 计算机状态:16个64位通用寄存器,1个64位程序计数器(PC),内存(Memory),条件码寄存器(Condition Code)
  • 程序的表示:C语言→汇编语言→二进制代码,汇编代码格式,寻址模式 I m m ( E b , E i , s ) Imm(E_b,E_i,s)
  • 各种指令:mov,lea,算数和逻辑,jmp等等
  • 控制:条件分支,循环,跳转表
  • 过程调用:传送控制、数据,分配和释放空间

程序编码

从一个C程序最终编译到一个可执行文件的过程,在后面第七章会详细讲解。在这个地方就先贴个图。
在这里插入图片描述

机器级代码

ISA:指令集体结构或指令集架构

对于我们而言,需要关注的就是:

  • Program counter(PC):程序计数器。在x86-64中用%rip表示。给出了简要执行的下一条指令
  • Register file:整数寄存器。我觉得这个是为了加速所存在的,里面一般放一些经常用的或者最近用过的数据
  • Condition code(CC):条件码。
  • Memory:内存。一些2进制地址数组,代码或者数据以及栈等等。
  • Vector file:向量寄存器。这些寄存器可以存放一个或者多个整数、浮点数的值。

代码示例

这节需要熟悉几个指令

  • 产生汇编的指令:gcc -Og -S xxx.c
  • 产生可执行文件的指令:gcc -Og -c xxx.c
  • 产生反汇编的指令:objdump -d xxx.o
  • gdb调试指令:(x/14b) xxx
    这些指令在之后的bomblab中会用到
    当然,为了尽可能使得汇编之后的代码和源代码差距不大,我们只用了-Og而没有用-O1 -O2 -Ofast。后面这些指令涉及到编译优化,优化后的代码和源代码差距比较大。

数据格式

整数类型数据大小:1,2,4,8 byte
浮点类型数据大小:4,8,10 byte

C声明 Inter数据类型 汇编代码后缀
char 字节 b
short w
int 双字 l
long 四字 q
char*(pointer) 四字 q
float 单精度 s
double 双精度 l

那你可能就要问了,咋区分int和double啊……long和pointer都还好,毕竟这两个大小都不一样……
整数寄存器和浮点寄存器是不一样的操作指令,所以……一样也没有关系啊……

访问信息

P120 图3-2的整数寄存器一定要背一定要背一定要背!!!不仅仅是寄存器名称,还有他的特殊作用。总之整张表都要背!!!
书上只有关于x86-64的寄存器,但是介于考试的时候会考32位,32位机器的寄存器也要熟悉!
在这里插入图片描述
x86-64传参的时候前6个参数是在对应的寄存器里面,多的参数在栈里面。
IA32传参的时候所有参数都是在栈里面。

IA32有两个特别的寄存器,一个%esp(栈指针),一个%ebp(基指针只能这样直译了
至于这两个指针是怎么操作的,在后面会详细说明。

操作数类型

操作数类型 格式
立即数 $Imm
寄存器 r a r_a
存储器 I m m ( r b , r i , s ) Imm(r_b,r_i,s)

需要单独说明一下存储器。
相当于我们现在只有某一个数的地位,根据这个地址,我们去内存中找一下对应的数据。
I m m ( r b , r i , s ) Imm(r_b,r_i,s) 得到地位 I m m + R [ r b ] + R [ r i ] s Imm+R[r_b]+R[r_i]*s 。其中 R [ r ] R[r] 表示寄存器 i i 中存的值。
当然,上面那几个参数都是可以缺损的。
可以没有 I m m , r b , r i , s Imm,r_b,r_i,s 。但是 s s 一定要和 r b r_b 一起存,即不存在 ( r b , , s ) (r_b,,s)
同时 s s 只能为 1 , 2 , 4 , 8 1,2,4,8 (为什么?)(整数类型直接大小只有这几个)
课件上有强调 r i r_i 不能是rsp,但是呢,要这么写编译是可以过的……细细想来,好像这么写也没啥意义?

mov指令

格式:movX Source, Destination(X需要根据具体的指令进行修改)
将S的数据写到D去。
在这里插入图片描述
上图列举了合法的mov操作。
D不能是立即数。
mov不能进行从内存到内存的操作。内存操作比较慢

大多数情况中,MOV指令只会更新目的操作数指定的那些寄存器字节或者内存位置。唯一例外的是movl指令以寄存器为目的时,它会把该寄存器的高位4字节设置为0。
看一下书上的例子。
在这里插入图片描述

关于mov指令的扩展,教材上比较清楚。
有一个比较特殊的指令是ctlq,它是将%eax符号扩展到%rax。

lea指令

一个特别牛逼的指令,实际上是mov指令的变形。
最初是加载有效地址。leaq S,D相当于D=&S。
但是它还可以这么用:

long cal(long x)
{
	return x*12;
}

对应的汇编

leaq (%rdi,%rdi,2),%rax	# t<-x+x*2
salq $2,%rax			# t<<2

已知变量x的值已经存放在寄存器eax中,现在想把5x+7的值计算出来并存放到寄存器ebx中,如果不允许用乘法和除法指令,则至少需要多少条IA-32指令完成该任务?
1条就行了
直接用leal 7(%eax,%eax,4) %ebx

算术和逻辑操作

在这里插入图片描述
在这里插入图片描述
更多的指令请看书。
注意一下操作数的顺序。
(思考:为啥没有区分有符号和无符号?)
(因为……根本就没有必要啊2333)

控制操作

条件码

在这里插入图片描述

  • CF:进位标志。最高位产生进位或者借位。检查无符号操作的溢出。
  • ZF:零标志。
  • SF:符号标志。
  • OF:溢出标志。导致补码溢出——正溢出或者负溢出。
    注意:leaq操作不改变CC。

cmp指令

cmpq s 1 s_1 , s 2 s_2
通过计算 s 2 s 1 s_2-s_1 来比较大小

text指令

textq s 1 s_1 , s 2 s_2
计算 s 2 s_2 & s 1 s_1
常用操作

testq %rax, %rax//相当于清零操作

set指令

访问条件码并设置
在这里插入图片描述
看一个例子

int comp(data_t a,data_t b)
{
	return a<b;
}

对应的汇编

comp:
	cmpq %rsi, %rdi
	setl %al
	movzbl %al, %eax	#零扩展
	ret

jmp指令

跳转到一个地址
在这里插入图片描述
区分一下直接跳转和间接跳转。
jmp *%rax是将%rax中的值作为跳转目标。
而jmp *(%rax)是将(%rax)作为跳转目标,也就是要从内存中读取(%rax)
条件跳转只能是直接跳转。

对于跳转目标,有两种不同的表示方式。

  • 绝对地址。用4字节直接指定目标。
  • 相对地址。在当前的基础上移动若干字节。

来个例子:用条件控制来实现条件分支

long absdiff(long a,long b)
{
	if(x>y)return x-y;
	else return y-x;
}

对应的汇编
absdiff:
	cmpq %rsi, %rdi
	jle .L4
	movq %rdi, %rax
	subq %rsi, %rax
	ret
.L4:
	movq %rsi, %rax
	subq %rdi, %rax
	ret

在if语句中会使用分支预测,如果预测错误耗时会很大。所以另一种想法是用三目运算符,也就是用条件传送来实现条件分支。

cmov指令

条件传送指令,在C语言中和三目运算符相似。
在这里插入图片描述
所谓的条件传送,无非是先把条件分支的2个部分都先算出来,之后再根据条件选择其中一个。
省时,但是有风险。

  • 导致结果错误
  • 耗时更多
  • 不安全
val=x>0?x*=7:x+=3;	//结果错误
val=test(x)?work1(x):work2(x);	//耗时更多
val=p?*p:0;			//不安全

循环和跳转表

循环(for,do-while,while)和跳转表(switch-case)无非是对于jmp,cmov等指令的运用。
值得提一句的是,跳转表相当于根据case的值和一张表得到要跳转的地址。
读读代码自然就明白了,没什么好说的。

过程

过程提供了一职中封装代码的方式。过程包括:函数,方法,子例程,处理函数等等。
以P调用Q,Q执行完回到P为例。

  • 传递控制。函数调用时,将PC设置成Q的起始地址。Q执行结束返回时,又需要将PC设置成P调用Q后面的指令地址。
  • 传递数据。P向Q传递参数,Q向P传递返回值。
  • 分配和释放内存。Q需要一定的空间来运行,在Q运行完了之后又需要释放这部分空间。

push和pop指令

push压栈,pop弹栈。
之前也说过了,有一些数据是要放在栈里面的。
push和pop就是对栈的操作。
举个例子。

pushq %rbp
相当于
subq $8, %rsp
movq %rbp, (%rsp)
------------------------------------------------------
pop %rax
相当于
movq (%rsp), %rax
addq $8, %rsp

请注意是相当于不是等价于
push和pop是不会改变条件码CC的,而被拆解的操作是会改变CC的。

传送控制

在调用过程Q时,我们用call指令。
call label:跳到label处
在Q中返回时,用ret指令(相当C语言中的return)。

问题来了,ret到哪里去???
ret的时候%rsp直接赋值给PC。那么在一开始call的时候,我们就要把返回的地址压入栈中,在要ret的时候让这个返回值在栈顶。

传送数据

之前我们也提到过了,在x86-64中前6个参数在寄存器中,多的参数在栈中,压栈的顺序是倒着压的,最后一个参数更靠近栈底,第7个参数更靠近栈顶。

返回值在寄存器%rax中

关于过程的空间问题

在这里插入图片描述
一般来说,过程通过减小栈指针在栈上分配空间,分配的结果作为栈的一部分。释放空间,即增加栈指针。
当然,对于那些用malloc申请的空间,自然是在堆上分配。(这一部分后面会讲)

同时,寄存器是唯一被所有过程共享的资源。我们需要保证在调用完过程Q回到过程P的时候,寄存器是没有发生变化的。最直观的想法,把一些寄存器的值保留起来,丢到栈里面。
P和Q对于这个保存的工作又分了一下工,有一些是调用者(P)保存寄存器,一些事被调用者(Q)保存寄存器。

运行时栈

分析之后,我们的栈大概就是这样子。
在这里插入图片描述

关于IA32

我们讨论的都是X86-64,对于IA32其实区别也不大。

  • IA32所有参数都在栈里面
  • IA32还有一个%rbp指针是需要和%rsp一起维护的
    在这里插入图片描述

数据

数组

把数组理解成指针,指针咋操作的,数组就咋操作。
高维数组在内存中实际上是按照一维数组进行存储的。
注意一个高维数组在空间中是连续分布的,而另外一种看似和高维数组用法类似,其本质完全不同。

int a[3][4];

int aa[4], aa2[4], aa3[4];
int *b[3]={aa,aa2,aa3};

一些emzj就很喜欢在这里出些很难的题。
在这里插入图片描述
在这里插入图片描述

结构体

猜你喜欢

转载自blog.csdn.net/coding__cat/article/details/86528380