3.6 控制
3.6.1 条件码(Condition Code)
- CPU维护一组条件码寄存器,描述最近的算数或逻辑操作的属性,用于条件分支指令
- 图3-10中的整数运算,除了
leaq外,都有可能设置条件码- 比如:移位操作,进位标志CF设置为最后一个被移出的位,溢出标志OF设置为0
| 标志 | 说明 |
|---|---|
| CF | 进位标志。最近的操作最高位产生进位。用于检查无符号操作的溢出 |
| ZF | 零标志。最近的操作得出结果0 |
| SF | 符号标志。最近的操作的到的结果为负数 |
| OF | 溢出标志。最近的操作导致一个补码溢出(正溢出或负溢出) |
- 还有两类指令
CMP和TEST,只改变条件码,而不改变其它寄存器CMP:与SUB行为一样,两个操作数相减,但不影响目的寄存器。如果两个数相等,ZF=1;如果不等,用SF、OF判断两数大小TEST:与AND行为一样,但不影响目的寄存器。
【说明】操作数A < 操作数B,为什么等价于SF^OF == 1?
- 假设两个操作数的值分别是a和b,t=a-b
- case1:a=10,b=20,则a<b,且t<0。未发生溢出,此时SF=1,OF=0,SF^OF=1
- case2:a=20,b=10,则a>b,且t>0。未发生溢出,此时SF=0,OF=0,SF^OF=0
- case3:a=-2,b=127,则a<b,且t=127>0。发生正溢出,此时SF=0,OF=1,SF^OF=1
- case4:a=1,b=-128,则a>b,且t=-127<0。发生负溢出,此时SF=1,OF=1,SF^OF=0
- 综上所述,
SF^OF==1代表a<b。其它a<=b可表示为(SF^OF)|ZF == 1(图3-14)
3.6.2 访问条件码
- 使用
set指令读取sete %rax表示把ZF标志赋给raxsetl %rax表示把SF异或OF的结果赋值给rax
【例】书中例子
int compare(int a, int b) {
return (a < b);
}
【编译】
//int compare(int a, int b)
//a in %edi, b in %esi
compare:
cmpl %esi, %edi //a-b的结果存放到条件码寄存器中
setl %al //结果赋值给al寄存器
movzbl %al, %eax //零扩展
ret
3.6.3 跳转指令
- 分无条件跳转(
JMP)和有条件跳转(如:JE、JL)- 无条件跳转:操作数是标号,或者间接跳转到某个程序计数器PC处
- 有条件跳转:处理判断和
set一样
【例】书中例子
int absdiff(int x, int y) {
long result;
if (x < y) {
result = y - x;
} else {
result = x - y;
}
return result;
}
【编译】gcc -Og -S absdiff.c
//int absdiff(int x, int y)
//x in %edi, y in %esi
absdiff:
cmpl %esi, %edi //Compare x:y
jge .L2 //If x>=y, goto .L2
subl %edi, %esi
movslq %esi, %rax //result = y-x
ret
.L2:
subl %esi, %edi
movslq %edi, %rax //result = x-y
ret
3.6.4 跳转指令的编码
- 跳转指令在通过汇编器、链接器后,将跳转目标的相对地址转为绝对地址
【例】书中例子branch.c
long branch(long x) {
long result = x;
while (result > 0)
result >>= 1;
return result;
}
int main() {
branch(1);
return 0;
}
【编译】gcc -Og -S branch.c
//long branch(long x)
//x in %rdi
branch:
movq %rdi, %rax
jmp .L2
.L3:
sarq %rax
.L2:
testq %rax, %rax
jg .L3
rep ret
【反汇编】gcc -Og -c branch.c; objdump -d branch.o
0000000000000000 <branch>:
0: 48 89 f8 mov %rdi,%rax
3: eb 03 jmp 8 <branch+0x8>
5: 48 d1 f8 sar %rax
8: 48 85 c0 test %rax,%rax
b: 7f f8 jg 5 <branch+0x5>
d: f3 c3 repz retq
【反汇编】gcc -Og -o branch branch.c; objdump -d branch
00000000004004ed <branch>:
4004ed: 48 89 f8 mov %rdi,%rax
4004f0: eb 03 jmp 4004f5 <branch+0x8>
4004f2: 48 d1 f8 sar %rax
4004f5: 48 85 c0 test %rax,%rax
4004f8: 7f f8 jg 4004f2 <branch+0x5>
4004fa: f3 c3 repz retq
【注】可以看到经过链接器后,jmp的相对位置变为绝对位置,其余不变(这对理解汇编有好处)
【备注】rep ret:当ret指令通过跳转指令到达时,根据AMD的说法,处理器不能正确预测ret指令的目的。所以增加rep空操作,使得代码在AMD上运行更快。无视即可。
3.6.6 用条件传送来实现条件分支
【例】用条件传送来实现条件分支,提升性能
【编译】还是absdiff例子,gcc -O2 -S absdiff.c
//int absdiff(int x, int y)
//x in %edi, y in %esi
absdiff:
movl %esi, %edx //edx = y
movl %edi, %eax //eax = x
subl %edi, %edx //edx = y-x
subl %esi, %eax //eax = x-y
cmpl %esi, %edi
cmovl %edx, %eax //If x-y<0, eax=y-x
ret
【说明】
- 现代处理器采用了分支预测逻辑,猜测跳转指令是否会执行。如果猜测正确,会显著提高性能;猜测错误,则会招致很严重的惩罚。
- 使用条件控制转移语句
cmovl,可以提高代码的效率,这与现代处理器的流水线处理(pipelining)有关系。当然,并不是所有场景都适合于条件控制转移(书本148页)。条件传送指令有:
3.6.7 循环
汇编语言中没有while、for,而是通过条件测试和跳转指令实现循环。
- 书中讲述了C代码先转换为goto版本,再转为汇编的过程,包括如何优化。感觉没必要抠得很深。知道是用跳转实现的循环即可。
【例】do-while循环
【例】while循环
【例】for循环:类似,略
3.6.8 switch语句
C语言的switch语句,分支数量较多时,使用跳转表使得比if-else效率高(节省cmp,空间换时间)
【例】书中示例:定义了一个数组,每个case直接跳转到标号进行处理
【编译】对应的汇编语句
【声明】汇编代码中,跳转表用以下声明表示
3.7 过程(函数调用)
3.7.1 运行时栈
- 操作系统每个进程有自己的运行时栈。栈划分为栈帧,存放控制和数据信息等内容。当前正在执行函数的栈总是在栈顶。
- 假设有两个函数
P()和Q(),调用链:main()->...->P()->Q(),运行时栈情况如图3-25所示:- 存在三个栈帧:较早的帧(比如main)、P的栈帧、Q的栈帧
- 函数调用时,程序计数器PC指向Q的起始地址、P向Q提供函数实参、Q为局部变量分配空间
- 函数返回时,程序计数器PC指向P调Q后一条指令地址、Q返回P一个值、Q释放局部变量存储空间
- P的栈帧
- 返回地址:P调用Q时,会将返回地址压入占中。Q返回时,P从那个地址继续向下执行
- 参数传递:如果Q()形参小于等于6个参数(指针和整数),可以只用寄存器实现。如果形参超过6个,需要将参数存放到P的栈帧中。
- Q的栈帧:保存寄存器的值、分配局部变量空间、为它调用的函数设置参数
3.7.2 转移控制——CALL和RET
call:函数调用。等价于- 将函数执行完成后的下一条地址压入栈中
- 跳转到函数的首地址执行
ret:函数调用返回。等价于- 从栈弹出地址,赋值给程序计数器%rip
- 跳转到这个地址继续执行
【例】书中例子,main()->multstore()->mult2()
#include <stdio.h>
long mult2(long a, long b) {
return a * b;
}
void multstore(long x, long y, long *dest) {
long t = mult2(x, y);
*dest = t;
}
int main() {
long d;
multstore(2, 3, &d);
printf("2 * 3 --> %ld\n", d);
return 0;
}
【反汇编】
000000000040055d <mult2>:
40055d: 48 89 f8 mov %rdi,%rax
400560: 48 0f af c6 imul %rsi,%rax
400564: c3 retq //pop %rip,rip=40056e,继续运行
0000000000400565 <multstore>:
400565: 53 push %rbx
400566: 48 89 d3 mov %rdx,%rbx
400569: e8 ef ff ff ff callq 40055d <mult2> //push 40056e,jmp 40055d
40056e: 48 89 03 mov %rax,(%rbx)
400571: 5b pop %rbx
400572: c3 retq //pop %rip, rip=40058b,继续执行
0000000000400573 <main>:
......
40057c: be 03 00 00 00 mov $0x3,%esi
400581: bf 02 00 00 00 mov $0x2,%edi
400586: e8 da ff ff ff callq 400565 <multstore> //push 40058b, jmp 400565
40058b: 48 8b 54 24 08 mov 0x8(%rsp),%rdx
......
3.7.3 数据传送(参数传递)
- 函数参数<=6:使用通用寄存器传递函数参数。注意这里的寄存器有特殊规定。
- 函数参数>6:1~6使用寄存器传递,多于6的部分通过栈来传递。
- 函数返回:通过
%rax返回
【注意】使用栈传参时,所有数据的大小按8字节对齐,这里a4虽然是char类型,在栈存储时占8字节空间
3.7.4 栈上的局部存储(局部变量)
- 比较简单的函数中,局部变量可以放在寄存器中。但是有些情况,局部数据必须存放在栈中:
- Case 1:寄存器不足,不够存放所有的本地数据
- Case 2:局部变量使用了取地址运算符'&',产生一个地址
- Case 3:局部变量是数组或结构体
【例】书中例子
void proc(long a1, long *a1p, int a2, int *a2p, short a3, short *a3p, char a4, char *a4p);
long call_proc() {
long x1 = 1;
int x2 = 2;
short x3 = 3;
char x4 = 4;
proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
return (x1+x2)*(x3-x4);
}
【编译】
call_proc:
subq $32, %rsp //分配了32字节的栈帧
movq $1, 24(%rsp) //x1:%rsp+24 ~ %rsp+31字节
movl $2, 20(%rsp) //x2:20~23字节
movw $3, 18(%rsp) //x3:18~19字节
movb $4, 17(%rsp) //x4:17字节
leaq 17(%rsp), %rax //参数8:&x4
movq %rax, 8(%rsp)
movl $4, (%rsp) //参数7:x4
leaq 18(%rsp), %r9 //参数6:&x3
movl $3, %r8d //参数5:x3
leaq 20(%rsp), %rcx //参数4:&x2
movl $2, %edx //参数3:x2
leaq 24(%rsp), %rsi //参数2:&x1
movl $1, %edi //参数1:x1
call proc //call proc()
movslq 20(%rsp), %rdx //rdx = x2
addq 24(%rsp), %rdx //rdx = x1+x2
movswl 18(%rsp), %eax //eax = x3
movsbl 17(%rsp), %ecx //ecx = x4
subl %ecx, %eax //eax = eax-ecx = x3-x4
cltq //convert to long
imulq %rdx, %rax //(x1+x2)*(x3-x4)
addq $32, %rsp //释放32字节的局部变量
ret
【说明】栈帧情况:这里出现Case 1和Case 2。注意:局部变量按字节对齐;传参按8字节对齐
3.7.5 寄存器中的局部存储空间
- 由于16个通用寄存器是共享的,所以函数调用时,被调用函数用到的寄存器可能会被覆盖,所以需要压栈保存。根据函数调用过程,寄存器分成两类:
- 被调用者保存:%rbx、%rbp、%r12~%r15(可能需压栈)
- 调用者保存:%rax、%rcx、%rdx、%rsi、%rdi、%r8~%r11(无需压栈)
【例】书中例子:这里由于P()中用到了%rbp和%rbx,所以要将环境的%rbp和%rbx压栈保存起来,最后函数返回的时候从栈恢复。注意压入顺序和弹出顺序是相反的(栈后进先出)。
3.7.6 递归过程
- 递归:函数调用自己本身。实现和调用其他函数时一样的
【例】计算阶乘,每递归一次都会有一个栈帧
【注意】如果递归次数太多不建议用递归,效率低且容易栈溢出