条件分支表达式的变量不能是一样的,例如val = x >0 ? x*= 7 : x+=3;会先计算两个分支的x值并更新,导致结果不对
条件分支指令的举例:有如下C语言指令:
long absdiff(long x, long y) { long result; if (x > y) result = x-y; else result = y-x; return result; }其对应的汇编语言如下:
absdiff: movq %rdi, %rax # x subq %rsi, %rax # result = x-y movq %rsi, %rdx subq %rdi, %rdx # eval = y-x cmpq %rsi, %rdi # x:y cmovle %rdx, %rax # if <=, result = eval ret寄存器的使用如下:
寄存器用于存储%rdix%rsiy%rax返回值对于每一步的操作如下表所示:
指令%rdi%rsi%rax%rdx备注初始状态xy//movq %rdi, %raxxyx/subq %rsi, %raxxyx-y/%rax减去%rsi,结果存储在%raxmovq %rsi, %rdxxyx-yysubq %rdi, %rdxxyx-yy-xcmpq %rsi, %rdixyx-yy-x比较%rdi和%rsi的大小,相当于根据%rdi-%rsi的结果更新状态寄存器cmovlexyx-y 或 y-xy-x根据条件码的情况,判断%rdi和%rsi的大小,如果%rdi<=%rsi,则将%rdx移动到%rax;否则不进行移动retxyx-y 或 y-xy-x返回%rax寄存器Do-While语句表示至少先执行一次,然后再进行条件判断
下述代码块中的过程,就用到了Do-While形式,此代码表示统计一个二进制数的1的个数:
long pcount_do(unsigned long x) { long result = 0; do { result += x & 0x1; x >>= 1; } while (x); return result; }将其等价转换为goto形式:
long pcount_goto(unsigned long x) { long result = 0; loop: result += x & 0x1; x >>= 1; if(x) goto loop; return result; }对应的汇编语言为:
movl $0, %eax # result = 0 .L2: # loop: movq %rdi, %rdx andl $1, %edx # t = x & 0x1 addq %rdx, %rax # result += t shrq %rdi # x >>= 1 jne .L2 # if (x) goto loop rep; ret寄存器的使用:
寄存器用于存储%rdix%rax返回值每一步操作后的状态(这里只列出从头开始的第一次循环):
指令%rdi%rax%rdx备注初始状态x//movl $0, %eaxx0/movq %rdi, %rdxx0xandl $1, %edxx01 & x1和%edx进行按位与运算,结果存入%edxaddq %rdx, %raxx1 & x1 & x将%rdx加到%rax上shrq %rdix >> 11 & x1 & x%rdi右移一位(逻辑移位)jne .L2x >> 11 & x1 & x判断条件码~ZF(x移位后的结果为非零)为真时,跳转至.L2处rep; ret0最终结果/返回%raxrep:某些AMD处理器的分支预测变量在分支的目标或穿透是ret指令时表现不佳,而添加rep前缀可以避免这种情况(https://www.codenong.com/20526361/)
for一定能用while实现,反之不然
在使用For语句表示Do-While时,可以进行优化,即去除进入第一次循环之前的条件判断
实现switch时,使用了跳转表,实现无条件跳转 跳转表:在表中按索引找到表项,表项存储的是地址 跳转表数据是只读形式的
如图1所示,switch的case值最小为0,最大为n-1时,跳转表(Jump Table)共有n个区域(每个区域占用8个字节),每一个区域指向一个代码块的起始地址(Jump Targets中对应的位置)。这样的过程可以用goto *JTab[x]描述,即跳转到JTab[x]所指向的地址处。
图1下面是一个用到了switch语句格式的例子:
long switch_eg(long x, long y, long z) { long w = 1; switch(x) { case 1: w = y*z; break; case 2: w = y/z; /* Fall Through */ case 3: w += z; break; case 5: case 6: w -= z; break; default: w = 2; } return w; }对应的汇编语言为:
switch_eg: movq %rdx, %rcx cmpq $6, %rdi # x:6 ja .L8 # Use default jmp *.L4(,%rdi,8) # goto *JTab[x]相应的跳转表为:
.section .rodata .align 8 .L4: .quad .L8 # x = 0 .quad .L3 # x = 1 .quad .L5 # x = 2 .quad .L9 # x = 3 .quad .L8 # x = 4 .quad .L7 # x = 5 .quad .L7 # x = 6对于汇编语言的各行语句,解释如下:
语句含义movq %rdx, %rcx将%rdx赋给%rcxcmpq $6, %rdi根据%rdi-$6的结果,更新条件码ja .L8(~CF&~ZF)条件为真(rdi>6的判断结果为真)时,由于超出了switch语句的case范围,因此将会直接跳转到.L8的位置,即跳出循环jmp *.L4(,%rdi,8)无条件跳转至跳转表的8*%rdi地址处在跳转表中,.align 8表示按8个字节(64位)对齐
switch跳转表,会将case按顺序排列,并填补空缺的case(没有被列出的case,但值介于最大和最小之间的,都会分配到相应的空间) 当case条件隔得非常远时,会导致跳转表变得稀疏。这种情况下,会使用两级表,但对空间会有所占用
x86-64栈的大致结构如图2所示,栈从上往下扩展,堆从下往上扩展(上方为高地址)
图2寄存器%rsp会记录栈的最低地址(栈顶地址)
压栈操作:pushq Src,将Src压入栈中,并将%rsp的值-8
弹栈操作:popq Dest,将栈顶值弹出,传送给Dest,并将%rsp的值+8
函数(过程)之间的相互调用,会使得其对应的汇编语言较为复杂。例如,下述函数:
long mult2(long a, long b) { long s = a * b; return s; }对应的汇编语言为:
0000000000400550 <mult2>: 400550: mov %rdi,%rax # a 400553: imul %rsi,%rax # a * b 400557: retq # Return注意到这样的过程,是需要知道它所在的起始地址的。不知道地址,也不知道该从哪里开始调用
过程的调用和返回,是在栈的协助下进行的
调用过程的指令:call label 属于单操作数指令,可以拆分为: 第一步:push指令 第二步:将标签地址赋给ip(jmp指令)
例如,callq 400550 <mult2>表示调用首地址位于400550的过程mult2,调用后,先将下一条语句的地址进行压栈,再将调用的函数的起始地址传给寄存器%rip
返回的指令:ret 先出栈,将数据送到ip寄存器中,根据ip寄存器取指令,即为call指令的下一条指令。简单来讲,就是回到之前调用这个函数的地方,在那之后开始继续执行语句
超过六个参数前:分别使用六个寄存器%rdi、%rsi、%rdx、%rcx、%r8和%r9对参数进行存储 超过六个参数后:压栈。此后要从栈中去取
返回值存储在寄存器%rax中
所有高级语言都支持栈
栈帧:一个过程在栈里有一块区域,这块区域叫做帧
帧指针(%rbp):定位过程的帧,和%rsp一样,一直在变化,永远指向正在执行的过程的帧的起始地址
图3展示了%rsp和%rbp的位置:
图3x86-64/Linux栈帧的大致图像如图4所示:
图4caller表示调用者,而callee表示被调用者
调用过程时,局部变量会和过程的起始地址一同被压入栈中
例如:对于过程:
long call_incr() { long v1 = 15213; long v2 = incr(&v1, 3000); return v1+v2; }对应的汇编语言:
call_incr: subq $16, %rsp movq $15213, 8(%rsp) movl $3000, %esi leaq 8(%rsp), %rdi call incr addq 8(%rsp), %rax addq $16, %rsp ret先让栈顶指针扩充了16个字节,再用15213这一局部变量占据上方的8个字节