文章目录
3.3 数据格式
因为是从16位扩展成32位的,所以命名时将16位的类型作为基准,成为word。
字节数 | 汇编代码后缀 |
---|---|
1 | b |
2 | w |
4 | l |
8 | q |
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 +*+操作数)
- 直接跳转(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-expr
和else-expr
求值,但有时求值可能是错误的,比如从一个空的地址取值。 - 降低效率: 如果计算
then-expr
和else-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去执行。
在汇编代码中,分几步进行:
- 映射开关索引值从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 max−min,共 m a x − m i n + 1 max-min+1 max−min+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
可用直接寻址和间接寻址。