试图还原老师讲课的思路。
这节课就是讲汇编的。
Complete addressing mode, address computation
Complete Memory Addressing Modes:
D(Rb,Ri,S) Mem[Reg[Rb]+S*Reg[Ri]+ D]
举个例子,
(%edx,%ecx,4) 0xf000 + 4*0x100 0xf400
0x80(,%edx,2) 2*0xf000 + 0x80 0x1e080
lea指令的功能是将一个内存地址直接赋给目的操作数,mov指令的功能是传送数据
lea eax,[ebx+8]就是将ebx+8这个值直接赋给eax,
mov eax,[ebx+8]则是把内存地址为ebx+8处的数据赋给eax。
movl v, %eax 传送v值到eax
(movl $v, %edi 传送v地址到edi)
【
mov %eax,%esp
This copies the value in %eax into %esp.
mov %eax,(%esp)
This copies the value from %eax to the location in memory that %esp points to.
】
举个例子,
lea:Load Effective Address,加载有效地址
Sal:Shift Arithmetic Left,算术左移
Arithmetic operations
重点:一个汇编的案例
预备知识:
mov和lea的区别;
ESP(STACK POINTER)是栈顶指针,EBP(BASE POINTER)是存取堆栈指针
下面是按调用约定__stdcall 调用函数test(int p1,int p2)的汇编代码:
假设执行函数前堆栈指针ESP为NN
push p2 ;参数2入栈, ESP -= 4h , ESP = NN - 4h
push p1 ;参数1入栈, ESP -= 4h , ESP = NN - 8h
call test ;压入返回地址 ESP -= 4h, ESP = NN - 0Ch
//进入函数内
{
push ebp ;保护先前EBP指针, EBP入栈, ESP-=4h, ESP = NN - 10h
mov ebp, esp ;设置EBP指针指向栈顶 NN-10h
mov eax, dword ptr [ebp+0ch] ;ebp+0ch为NN-4h,即参数2的位置
mov ebx, dword ptr [ebp+08h] ;ebp+08h为NN-8h,即参数1的位置
sub esp, 8 ;局部变量所占空间ESP-=8, ESP = NN-18h
...
add esp, 8 ;释放局部变量, ESP+=8, ESP = NN-10h
pop ebp ;出栈,恢复EBP, ESP+=4, ESP = NN-0Ch
ret 8 ;ret返回,弹出返回地址,ESP+=4, ESP=NN-08h, 后面加操作数8为平衡堆栈,ESP+=8,ESP=NN, 恢复进入函数前的堆栈.
}
前两句类似于初始化,让ebp指向当前栈顶,地址加4,那么x地址为8,y为12,z为16,也就是说把ebp+8就找到x了。此时我们看Body部分的
第一句,movl 8(%ebp), %ecx,此时地址为ebp+8,取的是x,存到ecx中;
第二句,movl 12(%ebp), %edx,把y存到edx;
第三句,leal (%edx,%edx,2), %eax,将edx+2*edx,也就是3*edx存到eax中,
第四句,sall $4, %eax,将eax左移4位,效果就是3edx*2^4=48edx,也就是y*48;
第五句,leal 4(%ecx,%eax), %eax,就是ecx+eax+4就是x+4+48y,就是t3+t4,存到eax中,就是变量t5;
第六句,addl %ecx, %edx,就是ecx+edx=x+y,存到edx中;
第七句,addl 16(%ebp), %edx,就是(ebp+16)+ edx,就是z+(x+y),就是变量t2;
第八句,imull %edx, %eax,就是edx*eax,就是t2*t5=(x+y+z)*(x+4+48*y);
第九句,返回rval。
over。
不过,我有一个疑问,编译器怎么做计算上的微调的,那个移位还能理解,为了减少计算量,那个x+48y+4为什么这么调整?
存放好局部变量后的栈:
另一个汇编的案例
Body部分:
movl 12(%ebp),%eax # eax = y
xorl 8(%ebp),%eax # eax = x^y (t1)
sarl $17,%eax # eax = t1>>17 (t2)
andl $8185,%eax # eax = t2 & mask (rval) 2^13 – 7 = 8185
Control: Condition codes
这一节谈谈状态码。
CF Carry Flag (for unsigned)
SF Sign Flag (for signed)
ZF Zero Flag
OF Overflow Flag (for signed)
举两个例子:
第一个:
Implicit Setting(think of it as side effect):
addl/addq Src,Dest↔ t = a+b
CF set if carry out from most significant bit (unsigned overflow)
ZF set if t == 0
SF set if t < 0 (as signed)
OF set if two’s-complement (signed) overflow (a>0 && b>0 && t=0)
第二个:
Explicit Setting: Compare
cmpl b,a like computing a-b without setting destination
CF set if carry out from most significant bit (used for unsigned comparisons)
ZF set if a == b
SF set if (a-b) < 0 (as signed)
OF set if two’s-complement (signed) overflow (a>0 && b0 && (a-b)>0)
读取状态码
set指令
只改变最低位,其他三位不变。
举个例子:
movl 12(%ebp),%eax # eax = y
cmpl %eax,8(%ebp) # Compare x : y
setg %al # al = x > y
movzbl %al,%eax # Zero rest of %eax 这个指令的意思是move zero-extended byte to long
Conditional branches
Jumping
跳转到代码不同部分
还是举两个例子实在
例子1:
在C语言里这段代码可以转换成
这样对应就好理解汇编代码了。goto和jump是直接关联的。
edx里存x,eax里存y,然后比较x和y,less or equal的话y-x,否则x-y,最终结果都存在eax中。最终返回eax。
条件表达式:val = Test ? Then_Expr : Else_Expr;
一般转换:
可以看出这里有分支,只会执行一个分支。
这样不利于指令的流水,而且需要control transfer。所以IA32 & x86-64会采用下面这种Conditional Moves的方式:
下面就Conditional Moves我们再举一个x86-64指令集下的例子:
最后依据指令的意思是move if greater。
由于Conditional Moves是把所有情形都算出来,所以有时候会带来一些副作用,我们在使用时要避免。
1.分支计算太复杂。
val = Test(x) ? Hard1(x) : Hard2(x);
这样的话还不如用条件判断,只计算一个分支。
2.计算时会有风险。
val = p ? *p : 0;
指针p的有效的则获取其值,如果p为0 不就获取了错误的值吗,需谨慎处理
3.计算带有副作用。
val = x > 0 ? x*=7 : x+=3;
这里同时会改变x和val的值,所以要谨慎。
Loops
1.“Do-While”
body必执行一次。
2.“While”
3.“For”
执行流程:
for和while是可以互换的。
由此顺利得到其goto version:
这里唯一需要强调的点是,在编译时期,i=0,WSIZE>0,编译器可以确定i<WSIZE,所以将其优化掉了。
汇编代码:
movl 8(%ebp), %edi #x装进edi,这是变量x
movl $0, %eax #0装进eax,这个变量是result
movl $0, %ecx #0装进ecx 这个是变量i
movl $1, %edx #1装进edx
.L13:
movl %edx, %esi #1赋给esi
sall %cl, %esi #%cl是%cx的低8位,这里的意思是将1算数左移i位,这是变量mask
testl %edi, %esi #效果等同于x & mask,改变了标志位
setne %bl #这里其实是读取zero flag,如果不为0,bl就存1
movl %ebx, %esi #将bl的结果赋给esi
andl $255, %esi #esi的结果还是bl
addl %esi, %eax #把这个结果加上result
addl $1, %ecx #i++
cmpl $32, %ecx #把i个8*4=32比较
jne .L13 #非0就跳转,由于i是从0开始增长的,所以i-32是从负数增长到0的
注:
80386有如下寄存器:
1、8个32-bit寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp;
2、8个16-bit寄存器,它们事实上是上面8个32-bit寄存器的低16位:%ax,%bx,%cx,%dx,%di,%si,%bp,%sp;
3、8个8-bit寄存器:%ah,%al,%bh,%bl,%ch,%cl,%dh,%dl。它们事实上是寄存器%ax,%bx,%cx,%dx的高8位和低8位;
4、6个段寄存器:%cs(code),%ds(data),%ss(stack), %es,%fs,%gs;
5、3个控制寄存器:%cr0,%cr2,%cr3;
6、6个debug寄存器:%db0,%db1,%db2,%db3,%db6,%db7;
7、2个测试寄存器:%tr6,%tr7;
8、8个浮点寄存器栈:%st(0),%st(1),%st(2),%st(3),%st(4),%st(5),%st(6),%st(7)。
test和cmp指令运行后都会设置标志位。
test属于逻辑运算指令
功能:执行BIT与BIT之间的逻辑运算,测试(两操作数作与运算,仅修改标志位,不回送结果)。
Test对两个参数(目标,源)执行AND逻辑操作,并根据结果设置标志寄存器,结果本身不会保存。TEST AX, BX 与 AND AX, BX 命令有相同效果。
Test的一个非常普遍的用法是用来测试一方寄存器是否为空:
test ecx, ecx
jz somewhere
CMP属于算术运算指令
功能: 比较两个值(寄存器,内存,直接数值)
CMP比较.(两操作数作减法,仅修改标志位,不回送结果)。
cmp实际上是只设置标志不保存结构的减法,并设置Z-flag(零标志)。
这节课讲了地址的概念,地址的计算,操作符,控制语句(状态码),条件分支,条件move,循环(do…while,while,for),下一节课将switch,stack,call/return。