资源描述
Linux下的汇编与Windows汇编最大的不同就是第一个操作数是原操作数,第二个是目的操作数,而Windows下却是相反。
1、 基本操作指令
简单的操作数类型说明,一般有三种,
(1)立即数操作数,也就是常数值。立即数的书写方式是“$”后面跟一个整数,比如$0x1F,这个会在后面的具体分析中见到很多。
(2)寄存器操作数,它表示某个寄存器的内容,用符号Ea来表示任意寄存器a,用引用R[Ea]来表示它的值,这是将寄存器集合看成一个数组R,用寄存器表示符作为索引。
(3)操作数是存储器引用,它会根据计算出来的地址(通常称为有效地址)访问某个存储器位置。用符号Mb[Addr]表示对存储在存储器中从地址Addr开始的b字节值的引用。通常可以省略下标b。
图1表示有多种不同的寻址模式,一个立即数偏移Imm,一个基址寄存器Eb,一个变址或索引寄存器Ei和一个伸缩因子s。有效地址被计算为Imm+R[Eb]+R[Ei]*s,对于这中寻址方式,我们可以在数组或者结构体中进行对元素进行访问时会用到。
操作数类型
格式
操作数值
名称
立即数
$Imm
Imm
立即数寻址
寄存器
Ea
R[Ea]
寄存器寻址
寄存器
Imm
M[Imm]
绝对寻址
寄存器
(Ea)
M[R[Ea]]
间接寻址
寄存器
Imm(Eb)
M[Imm+R[Eb]]
(基址+偏移量)寻址
寄存器
(Eb,Ei)
M[R[Eb]+R[Ei]]
变址寻址
寄存器
M[Imm+R[Eb]+R[Ei]]
寄存器
(,Ei, s)
M[R[Ei] * s]
伸缩化的变址寻址
寄存器
Imm(,Ei, s)
M[Imm+R[Ei] * s]]
伸缩化的变址寻址
寄存器
(Eb,Ei, s)
M[R[Eb]+R[Ei] * s]
伸缩化的变址寻址
寄存器
Imm(Eb,Ei, s)
M[Imm+R[Eb]+R[Ei] * s]
伸缩化的变址寻址
图1 操作数格式
注:操作数可以是立即数值、寄存器值或是来自存储器的值,伸缩因子必须是1、2、4、或者是8。 从上面的图我们就可以大致了解操作数的类型了。
在操作指令中,最频繁使用的指令是执行数据传送的指令。对于传送指令的两个操作数不能都指向存储器位置(我的理解是一般存储器存储的都是地址,不能够对地址和地址进行操作)。将一个值从一个存储器位置拷到另一个存储器位置需要两条指令——第一条指令将源值加载到寄存器中,第二条将该寄存器值写入到目的位置。下面给出源操作数和目的操作数的五种可能组合。
1、movl $0x4050, %eax 立即数——寄存器
2、movl %ebp, %esp 寄存器——寄存器
3、movl (%edi, %ecx), %eax 存储器——寄存器
4、movl $-17, (%esp) 立即数——存储器
5、 movl %eax, -12(%ebp) 寄存器——存储器
注意这里的指令mov可能有不同的形式,不同平台的汇编一般是有些不一样的,但这并不会影响我们理解汇编,只要熟悉其细节之后就能够明白的。
指令
效果
描述
movl S, D
D ←S
传送双字
movl S, D
D ←S
传送字
movl S, D
D ←S
传送字节
movl S, D
D ←符号扩展(S)
传送符号扩展的字节
movl S, D
D ←零扩展(S)
传送零扩展的字节
pushl S
R[%esp] ← R[%esp] – 4;
M[R[%esp]] ← S
压栈
popl D
D ← M[R[%esp]];
R[%esp] ← R[%esp] + 4
出栈
图2:数据传送指令
结合例子来进行讲解一下指令的具体操作,在这里将会正式接触到Linux下的GCC开发环境和GDB调试器,不过都是比较简单的应用。我的Linux操作系统是Ubuntu9.10,其它版本的差别应该不大,
如果我们要编写一个程序,我们可以用Linux下自带的vi或vim编辑器,
studyrush@studyrush-desktop:~/C$ vi exchange.c
vi 后面加我们要创建的程序文件的名字,在这里是exchange.c
studyrush@studyrush-desktop:~/C$ gcc -o exchange exchange.c
gcc -o exchange exchange.c 或gcc exchange –o exchange这两者都可以对源文件进行编译,-o exchange 表示对我们要输出的文件名称,可能表达的不够准确,大家可以先熟悉一下gcc编译器,应该就会明白的了。
studyrush@studyrush-desktop:~/C$ ./exchange 点加斜线再加输出文件名就表示运行程序,下面是运行的结果。
a = 3, b = 4
使用GDB可以参考附件里面的教程,这份教程写的很不错,看雪论坛本身也有,大家可以看这个贴(
studyrush@studyrush-desktop:~/C$ gdb exchange
命令disas就表示反汇编(disassembly),后面再加上要显示的函数名,我们就可以看到函数对应的反汇编代码了。
(gdb) disas exchange
Dump of assembler code for function exchange:
0x080483c4 <exchange+0>: push %ebp
0x080483c5 <exchange+1>: mov %esp,%ebp
0x080483c7 <exchange+3>: sub $0x10,%esp
0x080483ca <exchange+6>: mov 0x8(%ebp),%eax
0x080483cd <exchange+9>: mov (%eax),%eax
0x080483cf <exchange+11>: mov %eax,-0x4(%ebp)
0x080483d2 <exchange+14>: mov 0x8(%ebp),%edx
0x080483d5 <exchange+17>: mov 0xc(%ebp),%eax
0x080483d8 <exchange+20>: mov %eax,(%edx)
0x080483da <exchange+22>: mov -0x4(%ebp),%eax
0x080483dd <exchange+25>: leave
0x080483de <exchange+26>: ret
End of assembler dump.
进一步来分析上面的反汇编代码,这里的代码可能与前面的讲的指令操作有一些不同,因为这是很正常的,不同的操作系统应该有所差别,这也是汇编语言一般与平台有关,移植性并不好。
push %ebp
mov %esp,%ebp
sub $0x10,%esp
对有过反汇编的人对上面的代码肯定不会陌生,这里就是将ebp进栈,保存esp的值,并为局部变量预留空间。要记得源操作数与目的操作数的位置,前面有讲的。这里也一定程度对C语言中的指针使用进行了说明。看源代码可以知道。
mov 0x8(%ebp),%eax
mov (%eax),%eax
mov %eax,-0x4(%ebp)
mov 0x8(%ebp),%eax 取得xp,其实是指针本身,即是地址,汇编表示即可为mov %ebp + 8, %eax,这里就是存储器——寄存器的操作方式。
mov (%eax),%eax 这里是取得指针指向的值,即*xp,(%eax)表示取%eax中的地址指向的值,比如
寄存器 值 地址 值
%eax 0x100 0x100 0xFF
(%eax)的值就是0xFF了。
mov %eax,-0x4(%ebp) 这里是将值赋给局部变量,局部变量是在ebp的上面的,表示其地址比ebp的地址要小。
mov 0x8(%ebp),%edx
mov 0xc(%ebp),%eax
mov %eax,(%edx)
这里与上面的分析类似,并没有出现局部变量,可以表明这里是函数参数在进行交换。这里的操作数方式是寄存器——存储器
mov -0x4(%ebp),%eax 通过寄存器eax将值返回,记住一般函数的返回值都是通过eax寄存器返回的。
0x080483dd <exchange+25>: leave
0x080483de <exchange+26>: ret 函数返回
分析完反汇编代码之后有没有看出来函数调用是采用哪种调用约定呢?其实就是C调用方式(__cdecl)的方式。
//程序exchange.c,表示交换两个数。
#include <stdio.h>
#include <math.h>
int exchange(int *xp, int y)
{
int x = *xp;
*xp = y;
return (x);
}
int main()
{
int a = 4;
int b = exchange(&a, 3);
printf("a = %d, b = %d\n", a, b);
return 0;
}
通过上面的学习我们可以明白其实C语言中的指针本质上就是地址。抓住这一点我们对指针的理解就会更加明白。当然复杂的指针操作还是要在实践之中慢慢的掌握。
2、 算术和逻辑操作
加载有效地址(Load Effective Address)指令lea实际上是mov指令的变形,因为mov不能够直接对两个存储器操作数,,指令将有效地址写入到目的操作数(如寄存器)。举个例子:
如果寄存器%edx的值为x,那么指令lea 7(%edx, %edx, 4), %eax将设置寄存器%eax的值为5x+7,在这里需要注意目的操作数必须是一个寄存器。
指令
效果
描述
leal S , D
D ← &S
加载有效地址
incl D
decl D
negl D
notl D
D ← D + 1
D ← D-1
D ← -D
D ← ~D
加 1
减 1
取负
取补
addl S , D
subl S , D
iImull S , D
xorl S , D
orl S , D
and S , D
D ← D + S
D ← D - S
D ← D * S
D ← D ^ S
D ← D | S
D ← D & S
加法
减法
乘法
异或
或
与
sall k , D
shll k , D
sarl k , D
shrl k , D
D ← D << k
D ← D << k
D ← D >> k
D ← D >> k
算术左移
逻辑左移(等同于sal)
算术右移(符号位扩展)
逻辑右移 (0扩展)
图 3: 整数算术操作
注:后缀l表示对双字进行操作,还可以为w和b分别表示字和字节。
练习一下下面的移位操作指令
#include <stdio.h>
int shiftlr(int x, int n)
{
x <<= 2;
x >>= n;
return x;
}
int main()
{
int a = 4, b = 3;
int ans = shiftlr(a, b);
printf("%d\n", ans);
return 0;
}
shiftlr函数反汇编代码:
0x080483c4 <shiftlr+0>: push %ebp
0x080483c5 <shiftlr+1>: mov %esp,%ebp
0x080483c7 <shiftlr+3>: shll $0x2,0x8(%ebp)
0x080483cb <shiftlr+7>: mov 0xc(%ebp),%ecx
0x080483ce <shiftlr+10>: sarl %cl,0x8(%ebp)
0x080483d1 <shiftlr+13>: mov 0x8(%ebp),%eax
0x080483d4 <shiftlr+16>: pop %ebp
0x080483d5 <shiftlr+17>: ret
从上面的代码我们可以看到,移位量用单个字节编码,因为只允许进行0到31位的移位。位移量可以是一个立即数,或者放在单字节寄存器元素%cl中。
指令
效果
描述
imull S
mull S
R[%edx] : R[%eax] ← S ×R[%eax]
R[%edx] : R[%eax] ←S ×R[%eax]
有符号全64位乘法
无符号全64位乘法
cltd S
R[%edx] :R[%eax] ←符号扩展(R[%eax] )
转换为四字
Idivl S
R[%edx] ← R[%edx] : R[%eax] mod S
R[%edx] ← R[%edx] : R[%eax] ÷ S
有符号除法
divl S
R[%edx] ← R[%edx] R[%eax] mod S
R[%edx] ← R[%edx] : R[%eax] ÷ S
无符号除法
图4:特殊的算术操作
给个例子和其相应的反汇编代码练习一下。
#include <stdio.h>
int main()
{
int a = 6, b = 3;
unsigned int c = 10, d = 2;
int ans1 = a / b;
unsigned int ans2 = c * d;
return 0;
}
0x08048394 <main+0>: lea 0x4(%esp),%ecx
0x08048398 <main+4>: and $0xfffffff0,%esp
0x0804839b <main+7>: pushl -0x4(%ecx)
0x0804839e <main+10>: push %ebp
0x0804839f <main+11>: mov %esp,%ebp
0x080483a1 <main+13>: push %ecx
0x080483a2 <main+14>: sub $0x28,%esp
0x080483a5 <main+17>: movl $0x6,-0x8(%ebp)
0x080483ac <main+24>: movl $0x3,-0xc(%ebp)
0x080483b3 <main+31>: movl $0xa,-0x10(%ebp)
0x080483ba <main+38>: movl $0x2,-0x14(%ebp)
0x080483c1 <main+45>: mov -0x8(%ebp),%eax
0x080483c4 <main+48>: mov %eax,-0x2c(%ebp)
0x080483c7 <main+51>: mov -0x2c(%ebp),%edx
0x080483ca <main+54>: mov %edx,%eax
0x080483cc <main+56>: sar $0x1f,%edx
0x080483cf <main+59>: idivl -0xc(%ebp)
0x080483d2 <main+62>: mov %eax,-0x18(%ebp)
0x080483d5 <main+65>: mov -0x10(%ebp),%eax
0x080483d8 <main+68>: imul -0x14(%ebp),%eax
0x080483dc <main+72>: mov %eax,-0x1c(%ebp)
0x080483df <main+75>: mov $0x0,%eax
0x080483e4 <main+80>: add $0x28,%esp
0x080483e7 <main+83>: pop %ecx
0x080483e8 <main+84>: pop %ebp
0x080483e9 <main+85>: lea -0x4(%ecx),%esp
0x080483ec <main+88>: ret
2、 控制结构
CF:进位标志。最近的操作使最高位产生了进位,它可用来检查无符号操作数的溢出。
ZF:零标志。最近的操作得出的结果为0。
SF:符号标志。最近的操作得到的结果为负数。
OF:溢出标志。最近的操作导致一个二进制补码溢出——正溢出或负溢出。
整型变量a、b和t,用add指令完成等价于C表达式 t = a + b的功能。会根据下面的表达式来设置条件码:
CF:(unsigned t) < (unsigned a) 无符号溢出
ZF: (t == 0) 零
SF: (t < 0) 负数
OF: (a < 0 == b < 0) && (t < 0 != a < 0) 有符号溢出
lea指令不改变任何条件码,因为它是用来进行地址计算的。
指令
基于
描述
cmpb S2, S1
testb S2, S1
S1 – S2
S1 & S2
比较字节
测试字节
cmpw S2, S1
testw S2, S1
S1 – S2
S1 & S2
比较字
测试字
cmpl S2, S1
testl S2, S1
S1 – S2
S1 & S2
比较双字
测试双字
图 5: 比较指令
cmpb、cmpw、cmpl指令根据它们的两个操作数之差来设置条件码。
testb、testw、testl指令会根据它们的两个操作数的与(AND)来设置零标志和负数标志。
指令
同义名
效果
设置条件
sete D
setne D
setz
setnz
D ← ZF
D ← ~ZF
相等/零
不等/非零
sets D
setns D
D ← SF
D ← ~SF
负数
非负数
setg D
setge D
setl D
setle D
setnle
setnl
setnge
setng
D ← ~(SF ^| OF) & ~ZF
D ← ~(SF ^| OF)
D ← SF ^| OF
D ← (SF ^ OF) | ZF
大于(有符号 >)
大于等于(有符号 >=)
小于(有符号 <)
小于等于(有符号 <=)
seta D
setae D
setb D
setbe D
setnbe
segnb
setnae
setna
D ← ~CF & ~ZF
D ← ~CF
D ← CF
D ← CF | ZF
超过(无符号)
超过或相等(无符号 >=)
低于(无符号<)
低于或相等(无符号 <=)
图 6: sett指令
每条指令根据条件码的某个组合,将一个字节设置为0或者1。有些指令有“同义名”,也就是,同一条机器指令有别的名字
结合上面的说明练习一下。
#include <stdio.h>
char ctest(int a, int b, int c)
{
char t1 = a < b;
char t2 = b < (unsigned)a;
char t3 = (short)c >= (short)a;
char t4 = (char)a != (char)c;
char t5 = c > b;
char t6 = a > 0;
int sum = t1 + t2 + t3 + t4 + t5 + t6;
return sum;
}
int main()
{
int x = 5, y = 6, z = 7;
int ans = ctest(x, y, z);
printf("%d\n", ans);
return 0;
}
(gdb) disas ctest
0x080483c4 <ctest+0>: push %ebp
0x080483c5 <ctest+1>: mov %esp,%ebp
0x080483c7 <ctest+3>: sub $0x10,%esp
0x080483ca <ctest+6>: mov 0x8(%ebp),%eax
0x080483cd <ctest+9>: cmp 0xc(%ebp),%eax
0x080483d0 <ctest+12>: setl %al
0x080483d3 <ctest+15>: mov %al,-0x1(%ebp)
0x080483d6 <ctest+18>: mov 0xc(%ebp),%edx
0x080483d9 <ctest+21>: mov 0x8(%ebp),%eax
0x080483dc <ctest+24>: cmp %eax,%edx
0x080483de <ctest+26>: setb %al
0x080483e1 <ctest+29>: mov %al,-0x2(%ebp)
0x080483e4 <ctest+32>: mov 0x10(%ebp),%eax
0x080483e7 <ctest+35>: mov %eax,%edx
0x080483e9 <ctest+37>: mov 0x8(%ebp),%eax
0x080483ec <ctest+40>: cmp %ax,%dx
0x080483ef <ctest+43>: setge %al
0x080483f2 <ctest+46>: mov %al,-0x3(%ebp)
0x080483f5 <ctest+49>: mov 0x8(%ebp),%eax
0x080483f8 <ctest+52>: mov %eax,%edx
0x080483fa <ctest+54>: mov 0x10(%ebp),%eax
0x080483fd <ctest+57>: cmp %al,%dl
0x080483ff <ctest+59>: setne %al
0x08048402 <ctest+62>: mov %al,-0x4(%ebp)
0x08048405 <ctest+65>: mov 0x10(%ebp),%eax
0x08048408 <ctest+68>: cmp 0xc(%ebp),%eax
0x0804840b <ctest+71>: setg %al
0x0804840e <ctest+74>: mov %al,-0x5(%ebp)
0x08048411 <ctest+77>: cmpl $0x0,0x8(%ebp)
0x08048415 <ctest+81>: setg %al
0x08048418 <ctest+84>: mov %al,-0x6(%ebp)
0x0804841b <ctest+87>: movsbl -0x1(%ebp),%edx
0x0804841f <ctest+91>: movsbl -0x2(%ebp),%eax
0x08048423 <ctest+95>: add %eax,%edx
0x08048425 <ctest+97>: movsbl -0x3(%ebp),%eax
0x08048429 <ctest+101>: add %eax,%edx
0x0804842b <ctest+103>: movsbl -0x4(%ebp),%eax
0x0804842f <ctest+107>: add %eax,%edx
0x08048431 <ctest+109>: movsbl -0x5(%ebp),%eax
0x08048435 <ctest+113>: add %eax,%edx
0x08048437 <ctest+115>: movsbl -0x6(%ebp),%eax
0x0804843b <ctest+119>: lea (%edx,%eax,1),%eax
0x0804843e <ctest+122>: mov %eax,-0xc(%ebp)
0x08048441 <ctest+125>: mov -0xc(%ebp),%eax
0x08048444 <ctest+128>: leave
0x08048445 <ctest+129>: ret
指令
同义名
跳转条件
描述
jmp Label
jmp *Operand
1
1
直接跳转
间接跳转
je Label
jne Label
jz
jnz
ZF
~ZF
相等/零
不相等/非零
js Label
jns Label
SF
~SF
负数
非负数
jg Label
jge Label
jl Label
jle Label
jnle
jnl
jnge
jng
~(SF^OF) & ~ZF
~(SF^OF)
SF^OF
(SF^OF) | OF
大于(有符号 >)
大于或等于 (有符号 >=)
小于 (有符号 <)
小于或等于 (有符号 <=)
ja Label
jae Label
jb Label
jbe Label
jnbe
jnb
jnae
jna
~CF & ~ZF
~CF
CF
CF | ZF
超过 (无符号 >)
超过或相等 (无符号 >=)
低于 (无符号 <)
低于或相等 (无符号 <=)
图 7: 条件跳转指令
jmp指令是无条件跳转,它可以是直接跳转,即跳转目标是作为指令的一部分编码的,也可以是间接跳转,即跳转目标是从寄存器或存储器位置中读出的。看下面的两条指令:
jmp *%eax 用寄存器%eax中的值作为跳转目标。
jmp *(%eax) 以%eax中的值作为读地址,从存储器中读出跳转目标。
跳转指令有几种不同的编码,但是最常用的一些事PC相关的,也就是,它们会将目标指令的地址与紧跟在跳转指令后面那条指令的地址之间的差作为编码。这些地址偏移量可以编码为一、二或四个字节。第二种编码方法是给出“绝对”地址,用四个字节直接指定目标。
下面给出具体的例子进行一些分析,动手实践是必须要的,在计算机这一块中编程能力还是第一位的,这个也关系到自己以后职业的定位和发展。大家勤加练习,我也正在努力中。
C中的if-else语句的通用形式是这样的:
if (test-expr)
thenstatement
else
else-statement
这里test-expr是一个整数表达式,它的取值为0(解释为“假”)或者为非0(解释为“真”),两个分支语句(then-statement和else-statement)只会执行一个。
#include <iostream>
#include <cstdio>
using namespace std;
int absdiff(int x, int y)
{
if (x < y)
return y - x;
else
return x - y;
}
int main()
{
int m = 5, n = 6;
int ans = 0;
ans = absdiff(m, n);
printf("%d\n", ans);
return 0;
}
0x080485a4 <_Z7absdiffii+0>: push %ebp
0x080485a5 <_Z7absdiffii+1>: mov %esp,%ebp
0x080485a7 <_Z7absdiffii+3>: sub $0x4,%esp
0x080485aa <_Z7absdiffii+6>: mov 0x8(%ebp),%eax
0x080485ad <_Z7absdiffii+9>: cmp 0xc(%ebp),%eax
0x080485b0 <_Z7absdiffii+12>: jge 0x80485c1 <_Z7absdiffii+29>
0x080485b2 <_Z7absdiffii+14>: mov 0x8(%ebp),%edx
0x080485b5 <_Z7absdiffii+17>: mov 0xc(%ebp),%eax
0x080485b8 <_Z7absdiffii+20>: mov %eax,%ecx
0x080485ba <_Z7absdiffii+22>: sub %edx,%ecx
0x080485bc <_Z7absdiffii+24>: mov %ecx,-0x4(%ebp)
0x080485bf <_Z7absdiffii+27>: jmp 0x80485ce <_Z7absdiffii+42>
0x080485c1 <_Z7absdiffii+29>: mov 0xc(%ebp),%edx
0x080485c4 <_Z7absdiffii+32>: mov 0x8(%ebp),%eax
0x080485c7 <_Z7absdiffii+35>: mov %eax,%ecx
0x080485c9 <_Z7absdiffii+37>: sub %edx,%ecx
0x080485cb <_Z7absdiffii+39>: mov %ecx,-0x4(%ebp)
0x080485ce <_Z7absdiffii+42>: mov -0x4(%ebp),%eax
0x080485d1 <_Z7absdiffii+45>: leave
0x080485d2 <_Z7absdiffii+46>: ret
cmp 0xc(%ebp),%eax
jge 0x80485c1 <_Z7absdiffii+29>
mov 0x8(%ebp),%edx
mov -0x4(%ebp),%eax
从上面的三句我们可以看出这里就是执行if条件语句了。也可以看出if条件语句反汇编后的一些特征,我们一般可以认为cmp+条件跳转指令+jmp跳转,就是if条件语句反汇编的代码。这对于我们在实际中分析代码需要快速识别一些特征反汇编代码很有帮助。也方便我们对于代码的整体结构有更好的认识,记住一些常用的特征代码结构是必要的,这些也可以从正面入手,再反面进行分析,从而可以提高快速分析汇编代码的能力。
mov 0x8(%ebp),%edx 取得x
mov 0xc(%ebp),%eax 取得y
mov %eax,%ecx 把y放入寄存器%ecx中
sub %edx,%ecx 得到x – y
mov %ecx,-0x4(%ebp)
mov -0x4(%ebp),%eax 将结果放入%eax中返回
由于上面的跳转指令是jge即表示大于等于跳转,而if条件语句本身的比较是用<进行的,所以上面这段汇编代码是先进行else后面的语句体进行执行的。对代码进行分析即可以知道。另外一般函数的返回值都是用%eax寄存器返回的,看最后一句就可以知道了。
C语言提供了好几种循环结构,即while、for和do-while。汇编语言中并没有相应的指令存在,作为替代,将条件测试和跳转组合起来实现循环的效果。比较有趣的是,大多数汇编器根据一个循环的do-while形式来产生循环代码。
do-while循环
其通用形式是这样的:
do
body-statement
while (test-expr);
循环的效果就是重复执行body-statement,对test-expr求值,如果求值的结果为非零,就继续循环。注意,body-statement至少执行一次。
看看下面举的例子来进行分析。
#include <iostream>
#include <cstdio>
using namespace std;
int fib_dw(int n)
{
int i = 0;
int val = 0;
int nval = 1;
do {
int t = val + nval;
val = nval;
nval = t;
i++;
} while ( i < n);
return val;
}
int main()
{
int m = 5;
int ans = 0;
ans = fib_dw(m);
printf("%d\n", ans);
return 0;
}
展开阅读全文