0x01 函数
汇编眼中的函数,函数就是一系列指令的集合,为了完成某个会重复使用的特定功能。
可以使用JMP
指令或者CALL
指令来进行调用函数,先看JMP
指令。
JMP指令调用函数
假设定义一个函数功能为将eax,ecx
的值赋值为0
,假设使用JMP
来进行调用
此时就会出现一个问题,当通过JMP
调用了指令后,无法再次回到使用JMP
指令的地方,解决的话可以在函数中再次使用JMP
指令跳转回来。
但是这样做同样也会出现问题,回想函数的定义,重复使用的特定功能,那么下次再进行函数时,仍然会回到首次定义的JMP
地方,无法回到下次使用函数的地方,所以使用JMP
指令来调用函数就不太方便。
CALL指令调用函数
这里再使用CALL
指令来调用函数,由于CALL
指令会将当前指令的下一行存储在堆栈中,所以直接在函数的最下面进行ret
就可以回到之前执行函数的地方了。
运行后观察结果
函数的参数和返回值
以写一个加法的函数为例子
add eax,ecx
ret
这里的参数指的是就是eax
和ecx
,返回值就是eax
,如下
运行结果后,eax
应该为7
,同时指针回到0040ef44
,运行后观察结果。
0x02 堆栈传参
如果在参数很多的情况下,计数器可能不够用情况,此时就可以用堆栈进行传递参数。
这里以计算5
个参数值为例,先将值压入栈中
push 1
push 2
push 3
push 4
push 5
定义函数,此时应该要将最上层的栈的值给到eax
中,然后连续让eax
加上下面的几层栈存储的值
mov eax,dword ptr ds:[esp+4]
add eax,dword ptr ds:[esp+8]
add eax,dword ptr ds:[esp+C]
add eax,dword ptr ds:[esp+10]
add eax,dword ptr ds:[esp+14]
ret
运行测试
效果正常实现
堆栈平衡
虽然上述实验成功实现效果,但是存在一个小问题,最后堆栈并没有还原,也就是所谓的没有堆栈平衡
。
上述程序在运行前,栈的最上面是12ffc4
,但是函数运行结束后,则变成了12ffb0
针对上面的问题,第一个解决方案就是采用外平栈
,在call
指令后使用add esp,8
就可以恢复栈的原有值了。
当然还可以直接将ret
改为ret 8
(等同于ret后再add esp,8),实现函数内的栈平衡,称为内平栈
。
esp寻址
从上面的例子可以看到最终拿出之前压入栈中的值时,是以esp
为基址进行查找的,这种行为称为esp
寻址。
mov eax,dowrd ptr ss:[esp+8]
add eax,dowrd ptr ss:[esp+4]
ret
这种寻址方式有非常明显的好处,因为esp
寻找起来非常简单和直白。同样的,也是有存在缺点的。
假设某函数在使用时需要用寄存器,但是又无法将寄存器的值进行直接清空,需要保留,所以在执行函数前需要先保留寄存器中的值
push eax
push ecx
mov eax,dowrd ptr ss:[esp+8]
add eax,dowrd ptr ss:[esp+4]
ret
但是此时就会存在一个问题,由于push
指令改变了栈,所以此时esp
的值不能再直接去加了,而是要根据使用的指令情况来增加,这里由于使用了两个push
,所以整体函数变成了
push ecx
push edx
mov eax,dowrd ptr ss:[esp+C]
add eax,dowrd ptr ss:[esp+10]
ret
同时在使用完ecx
和edx
后也需要还原,所以还得继续使用pop
做堆栈平衡。
push ecx
push edx
mov eax,dowrd ptr ss:[esp+C]
add eax,dowrd ptr ss:[esp+10]
pop edx
pop ecx
ret
从这个例子中也能看到缺点,如果之前push
的指令比较多,影响了堆栈,那么在使用esp
寻址时就需要手动计算esp
的变更后的值,相对麻烦一些。
EBP寻址
从刚刚的情况中找到了不足,这里可以使用ebp
来进行寻址,ebp
是栈底指针。可以看下面的例子
push ebp
mov ebp,esp
sub esp,10
先将ebp
的值存储栈中以便后续还原,将着将ebp
设置到原有的esp
的位置,接着减少esp
的值,这样就可以重新扩展出一块堆栈了,使用时不会影响原有的栈。此时以ebp
来寻址的话,就不会再重新计算参数的位置了,因为在使用堆栈的时候ebp
的值是不会改变的。所以此时可以直接取值
mov eax,dword ptr ss:[ebp+4]
add eax,dowrd ptr ss:[bgp+8]
同时在完成函数后,还需要做平栈,还原ebp
和esp
。
mov esp,ebp
pop ebp
ret
虽然感觉多花了一些步骤,但是实际上如果函数步骤复杂,使用的堆栈较多的情况下,使用ebp
寻址还是很有优势的。
0x03 JCC指令
有条件修改eip
寄存器的指令,比如JMP
和CALL
都是无条件修改。
JCC
指令是通过查看标记寄存器来进行判断的
-
CF
,carry flag
主要用来判断无符号数计算以后是否溢出,如果发生进位或者借位则将其置1
,反之清零。
-
PF
,Parity flag
,如果结果的最低有效字节包含偶数个1位则置为1,否则清0,一般用于传递数值后的校验完整性 -
AF
,auxilary Carry flag
,如果算术操作在结果的第3位发生进行或者进位,则为1
,一般用于BCD
运算。 -
ZF
,zero flag
,如果运算结果为0
,则置为1
使用cmp
或者test
指令都会使用到此指令
cmp可以判断两数是否相等(相当于sub
,但是不把值进行存储)
test可以判断否数是否为0(相当于and
,也不存储数值)
-
SF
,Sigh flag
,有符号整数的最高有效位,0代表为正,1代表为负 -
OF
,Overflow flag
,有符号数加减运算所得结果是否溢出,溢出为1,反之为0
有符号数看of
,无符号数看cf
-
DF
,direction flag
,方向位,控制栈的传递方向,比如movs,stos
等指令,STD
和CLD
指令分别 用于设置以及清除DF
标志。
常见指令如下
JE,JZ,是结果为0则跳转,ZF=1
JNE,JNZ,是结果不为0则跳转,ZF=0
JS,结果为负则跳转,SF=1
JNS,结果为非负则跳转,SF=0
JP,JPE,结果中1的个数要是偶数则跳转,PF=1
JNP,JPO,结果中1的个数要是奇数则跳转,PF=0
JO:结果溢出则跳转,OF=1
JNO,结果未溢出则跳转,OF=0
JB,JNAE,是无符号数小于则跳转,CF=1
JNB,JAE,是无符号数大于等于则跳转,CF=0
JBE,JNA,是无符号数小于等于则跳转,CF=1 or ZF=1
JNBE,JA,是无符号数大于则跳转,CF=0 and ZF=0
JL,JNGE,是有符号数小于则跳转,SF!=OF
JNL,JGE,是有符号数大于等于则跳转SF=OF
JLE,JNG,是有符号数小于等于则跳转,ZF=1 or SF!=OF
JNLE,JG ,是有符号数大于则跳转 ZF=0 and SF=OF