深入理解计算机系统第三章

3.3 数据格式

因为是从16位扩展成32位的,所以命名时将16位的类型作为基准,成为word。

字节数汇编代码后缀
1b
2w
4l
8q

3.4 访问信息

在这里插入图片描述
在这里插入图片描述

2条小规则:

  • 生成1字节和2字节高位不变,生成4字节高位置0。
  • %rsp作为栈指针指明运行时栈结束的位置。

3.4.1 操作数指示符

指令的源数据值可以是常数,或者存储在寄存器和内存中,结果可以存储在寄存器和内存中。
操作数的三种类型:

  • 十六进制数字:表示立即数
  • R [ r a ] R[r_a] R[ra]:其中 r a r_a ra表示某个寄存器, R [ r a ] R[r_a] R[ra]表示寄存器中的数字,可为低1,2,4,8字节。
  • M [ A d d r ] M[Addr] M[Addr]:内存引用,可以将内存看成一个非常大的数组,表示从地址 A d d r Addr Addr开始的字节引用。
    在这里插入图片描述
    注意点:
  • 比例因子必须是1,2,4或8。
  • 除了第一个是立即数,第二个是寄存器,后面的类型都是存储器。
  • 出现比例因子或者两个寄存器:变址寻址;出现立即数和寄存器:(基址+偏移量)寻址;…
  • 出现括号就在存储器中找。
  • 在间接寻址的时候寄存器都要使用64位(r开头)。

3.4.2 数据传送指令

在这里插入图片描述
源操作数存储在寄存器或内存中,或者是立即数,目的操作数是寄存器或内存地址。
注意点:

  • 不能两个操作数都在内存中,若确实如此需要拆分成两条指令:内存->寄存器->内存。
  • m o v l movl movl会将目的寄存器的高位置0,当目的操作数是一个寄存器的时候。
  • 当源操作数超过32位补码的立即数时,且目的操作数是寄存器的时候,用 m o v a b s q movabsq movabsq,而不用 m o v q movq movq,因为它只能表示32位补码数字的立即数作为操作数字。但是注意 m o v a b s q movabsq movabsq的目的操作数不能是内存。

在这里插入图片描述
M O V Z , M O V S MOVZ,MOVS MOVZ,MOVS类指令注意点:

  • 没有 m o v z l q movzlq movzlq的指令,默认 m o v l movl movl就是这样操作的;
  • c l t q cltq cltq指令进行对 e a x eax eax r a x rax rax进行符号扩展,推测可能是因为 r a x rax rax经常作为返回值,这样定义一个指令使汇编代码更简洁。

3.4.3 数据传输示例

练习题3.4

void func(src_t *sp,dest_t *dp)
{	
	*dp=(dest_t)*dp;
}

第一个为src的类型,第二个为dest的类型,可能会发生大小和符号的转换,首先考虑大小的转换 ,即扩展时根据源操作数是否有符号进行扩展,如源操作数为有符号 c h a r char char,相应地需要用符号扩展 M O V S MOVS MOVS进行大小转换;如源操作数为无符号 u n s i g n e d unsigned unsigned,相应地需要用零符号扩展 M O V Z MOVZ MOVZ进行大小转换。

  • long -> long
    movq (%rdi), %rax
    movq %rax, (%rsi)
  • char ->int
    movsbl (%rdi), %eax
    movl %eax, (%rsi)
    第一个指令进行类型和大小的转换,第二个指令将转换好且放在寄存器中的数字放入内存,同时要判断转换后数字的字节数。此题不必进行符号转换,只有大小转换:第一步用 m o v s b l movsbl movsbl将有符号的1字节的 c h a r char char转换为有符号4字节的 i n t int int;第二步根据转换后 i n t int int的字节为4选择 m o v l movl movl指令将数字从寄存器移动到内存中。
  • char->unsigned
    movsbl (%rdi), %eax
    movl %eax, (%rsi)
    同时出现大小和符号的转换,第一步先不考虑符号转换,此时源操作数为有符号 c h a r char char,相应地需要用符号扩展 s b l sbl sbl进行大小转换;第二步则需考虑符号转换, 因为是从有符号变成无符号,因此相当于是零扩展,但是扩展指令中没有 m o v z l q movzlq movzlq的指令,因为默认 m o v l movl movl就是默认高位补0的,因此直接使用 m o v l movl movl
  • unsigned char->long
    movzbq (%rdi), %rax
    movq %rax, (%rsi)
  • int ->char
    movl (%rdi), %eax
    movb %al, (%rsi)
    此题从长字节变为短字节,不会发生扩展,第一步直接按照源操作数的长度截断;第二步按照目的操作数的长度进行存储。
  • unsigned -> unsigned char
    movl (%rdi), %eax
    movb %al, (%rsi)
    与上一题相似
  • char ->short
    movsbw (%rdi), %ax
    movw %ax, (%rsi)

总结
第一步操作考虑源操作数的符号和长度;
第二步操作考虑目的数的长度。

3.4.4 压入和弹出数据

  • 属性:后进先出
  • 栈顶:栈插入或删除元素的位置。如下图,栈顶在下,栈底在上。栈指针 %rsp 保存栈顶元素的地址。且栈顶元素的地址是所有栈中元素地址最低的
    在这里插入图片描述
  • 数据操作:压入数据需要减小栈指针后再存放数据,弹出数据需要先获取栈指针地址的元素再增加栈指针。
    在这里插入图片描述
  • 数据获取:不像stl的stack,只能获得栈顶元素,这里的栈可以用内存寻址的方式获得栈中任意位置的值,而不仅仅只能获得栈顶元素。比如栈元素是8个字节的,用指令$movq 8(%rsp), %rbx就能将栈中的第二个元素复制到寄存器%rbx中。

3.5 算数和逻辑操作

在这里插入图片描述

3.5.1 加载有效地址

  • 形式:leaq S,D(D必须是寄存器)
  • 功能:将S的有效地址写到寄存器D中,可以看成movq指令的变形,但是movq指令是获得地址S中的内容,而leaq是直接获得S的有效地址。
  • 用途:在内存引用中产生指针(暂未涉及),简洁描述普通算数操作,如下图:
    在这里插入图片描述

3.5.2 一元操作和二元操作

  • 一元操作:操作数既是源又是目的,有+1(INC),-1(DEC),取负(NEG),取补(NOT)。
  • 二元操作:
    第二个操作数是源又是目的,可以是寄存器或内存位置,并且作为第一个操作数。 如subq %rax, %rdx,最后%rdx存储的值为(%rdx的值-%rax的值)。

3.5.3 移位操作

指令:SAL/SHL(左移),SAR(算数右移,填上符号位),SHR(逻辑右移,填上0)。
操作数:第一个操作数可以是立即数,表示移位量;或者是寄存器%al(寄存器只能写%al而不能写别的)。此时移位量由%cl寄存器的低m位表示,其中 2 m = w 2^{m}=w 2m=w,w表示移位操作的位长,b,w,l,q对应的位长分别为8,16,32,64。当%cl的值为0xFF,salb指令w=8, 2 m = 2 3 = w = 8 2^{m}=2^{3}=w=8 2m=23=w=8,因此m=3,即移位量由%cl的低3位确定,该低三位为 ( 111 ) 2 = 7 (111)_{2}=7 (111)2=7,即移动量为7。同理salw移动15位,sall移动31位,salq移动63位。

3.5.5 特殊的算术操作

64位的整数相乘需要128位(16字节,8字)来保存,但是寄存器都是64位的,因此可用两个寄存器来保存8字的数据。先将一个乘数移动到在寄存器%rax中,将另一个乘数移动到S中,执行指令imulq S之后,%rax就保存乘积的低8字节,%rdx保存乘积的高8字节。若表示无符号的乘法,将imulq换成mulq即可。
因为普通的二元操作符不提供除法,因为整数除法也有两个结果需要保存:商和余数,一个源操作数不够保存,因此此时可将结果保存在指定寄存器中。先将被除数移动到寄存器%rax中,将除数移动到S中,执行指令idiv S之后,%rax保存商,%rdx保存余数。若表示无符号除法,将idivq换成mulq即可。
在这里插入图片描述

3.6 控制

3.6.1 条件码

一条指令执行等价的表达式(如add %rdi, %rsi等价于b=a+b)会更新一组条件码寄存器(但是leaq指令不会改变条件码因为它只是进行地址计算):
在这里插入图片描述
比如此时的b为0,则ZF零标志会被设置为1,表示最后的目的操作数被设置为0。

还有两组新的指令,专门设置条件码,而不改变其他任何寄存器。CMP组相当于S2-S1/TEST组相当于S1&S2,但是不改变S2的值,只用这个结果是否为进位,0,负数,溢出来设置条件码。
在这里插入图片描述

3.6.2 访问条件码

访问3.6.1 节获得的条件码可用来:

  • 设置字节(0/1)
  • 程序跳转(if-else)
  • 有条件地传送数据(赋值)
    访问条件码的指令其实是将目的操作数设置为条件码的组合,其后缀代表了其组合结果:
  • n(not) : 不是
  • z(zero) : 为0
  • g(greater)/a(above无符号) : 大于
  • e(equal) : 等于
  • l(lower)/b(below无符号) : 小于
    在这里插入图片描述
    注意在获取条件码,访问条件码以设置寄存器最低位字节后,需要把设置寄存器的高位都置零如movzbl %al %eax。 且应该注意有符号和无符号的区别(大于号小于号的区别其他相同)。

3.6.3 跳转指令

  • 无条件跳转(jmp)
    • 直接跳转(jmp+程序标号)
      在这里插入图片描述
    • 间接跳转(jmp +*+操作数)
      在这里插入图片描述
  • 有条件跳转(和SET指令一样neablg
    在这里插入图片描述

3.6.4 跳转指令编码

跳转指令最常用的是PC相对寻址,即将目标指令地址(要跳转到的地址)与当前跳转指令地址后一条指令的地址的差作为编码。 如下图右侧是汇编器形产生的".o"格式(左边二进制)的反汇编版本。
分析第2行的跳转指令,从反汇编版本可知是目标指令地址为0x8,当前跳转指令地址为0x3,且指令长度为2个字节(eb 03为两个字节),因此当前跳转指令地址的下一个指令地址为0x5(即当前跳转指令地址0x3+当前跳转指令长度2=0x5),跳转指令编码则为0x8-0x5=3。
在这里插入图片描述
如下图,代码在链接后被重定位到不同的地址,但是由于是相对寻址,汇编二进制码内容并不需要改变,这就是相对寻址优于绝对寻址的地方,因为目标代码不做改变就能移到内存中的不同位置
在这里插入图片描述

3.6.5 条件控制实现条件分支

前4节均在做铺垫,获取条件码→访问条件码→跳转指令→跳转编码。此节可实现c语言中的条件分支功能。
c语言if-else

if(test-expr)
	then-statement
else
	else-statement

等价的c语法汇编 if-else,注意在then-statement后需要加上goto done,防止继续执行false

if(!test-expr)
	goto false;
then-statement;
goto done;
false:
	else-statement;
done:

3.6.6 条件传送实现不同分支

在这里插入图片描述

在条件控制时,处理器会猜测每条跳转指令是否执行,但是在非常不可预测的if-else分支中,猜测错误会导致更多的时钟周期。因此可采用条件传送取代条件分支,因为它需要的时间比较短。因为处理器无需预测测试结果即可执行条件传送。
比如v=test-expr ? then-expr : else-expr,在这种判断之后进行不同赋值的语句即可用条件传送。若该语句用条件控制转移则为:

if(!test-expr)
	goto false;
v = then-expr;
goto done;
false:
	v = else-expr;
done:

若用条件传送则是:

v = then-expr;
ve = else-expr;
if(!test-expr) v=ve;

弊端:

  • 产生错误: 无论如何都会对then-exprelse-expr求值,但有时求值可能是错误的,比如从一个空的地址取值。
  • 降低效率: 如果计算then-exprelse-expr过程复杂,耗费的时间长于错误猜测的时间。
  • 条件受限: 只有分支中是赋值的情况才适合使用条件传送。

3.6.7 循环

do-while

首先执行循环体,然后进行测试,若测试满足条件,仍然跳转到循环开始处。

do 
	body-statement
while(test-expr);

goto语句:

loop:
	body-statement
	t = text-expr;
	if(t) 
		goto loop;
while
while(test-expr)
	body-statement

有两种实现while的方式:

  • jump to middle
    在循环开始前直接跳转到do-while中间的进行测试
goto test;
loop:
	body-statement;
	test:
		t = test-expr;
		if(t) goto loop;
  • guarded-do
    先用一个条件分支,若条件不满足直接跳过整个do-while
t = test-expr;
if(!t)goto done
loop:
	body-statement;
	t = test-expr;
	if(t) goto loop;
done:

两种方法只要看一开始是跳转到goto-loop之前(jump to middle)或者之后(guarded-do)则可区分。

for 循环

通用格式

for(init-expr;test-expr;update-expr)
	body-statement

可用while来实现for循环:

init-expr;
while(test-expr){
	body-statement
	update-expr;
}

此时可用jump to middle或者guarded-to方法用do-while实现while语句。

3.6.8 switch 语句

使用情况:当分支多(多于4个)且值的跨度小时,则使用跳转表。
跳转表(jump table)是一个数组,数组中存放的是执行指令的地址。为了实现跳转,要通过下标索引到数组中的位置,switch的开关索引值就是要查找的下标。比如jt={0x1,0x5,0x10},则当开关索引为0时,程序直接跳转到地址0x1去执行。
在汇编代码中,分几步进行:

  1. 映射开关索引值从0开始,比如开关索引值一共有 { 4 ( m i n ) , 5 , 7 ( m a x ) } \{4(min),5,7(max)\} {4(min),5,7(max)},则数组 j t = { 0 , 1 , 2 , 3 } jt=\{0,1,2,3\} jt={0,1,2,3},即数组为从0开始的连续递增自然数,到 m a x − m i n max-min maxmin,共 m a x − m i n + 1 max-min+1 maxmin+1个值。

3.7 过程

3.7.1 运行时栈

栈帧结构:

  • 栈底(高地址)
  • 更早的栈帧
  • P的栈帧
    • 参数7(最多向Q传6个参数,还需要更多的参数则放在P的栈帧中)
    • 参数…
    • 参数n
    • 返回地址
  • Q的栈帧
    • 保存寄存器(将寄存器的值放在栈中 返回时再赋值给原来对应的寄存器 保证其值不变)
    • 局部变量(寄存器不够存储局部变量/需要取地址操作的局部变量,即指针)
    • 参数构造区(Q可继续调用别的过程,像P调用Q那样)
  • 栈顶(高地址)

但是有的过程不需要栈帧,如果其局部变量用寄存器则足够存储且不调用其他过程。

3.7.2 转移控制

在P中地址A(下一条指令地址为B)处调用call Q,会将地址B(返回地址)压入栈中,并将PC设置为Q的起始地址。调用call Q可用直接寻址和间接寻址。

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值