汇编眼中的函数,函数就是一系列指令的集合,为了完成某个会重复使用的特定功能。
可以使用JMP
指令或者CALL
指令来进行调用函数,先看JMP
指令。
假设定义一个函数功能为将eax,ecx
的值赋值为0
,假设使用JMP
来进行调用
此时就会出现一个问题,当通过JMP
调用了指令后,无法再次回到使用JMP
指令的地方,解决的话可以在函数中再次使用JMP
指令跳转回来。
但是这样做同样也会出现问题,回想函数的定义,重复使用的特定功能,那么下次再进行函数时,仍然会回到首次定义的JMP
地方,无法回到下次使用函数的地方,所以使用JMP
指令来调用函数就不太方便。
这里再使用CALL
指令来调用函数,由于CALL
指令会将当前指令的下一行存储在堆栈中,所以直接在函数的最下面进行ret
就可以回到之前执行函数的地方了。
运行后观察结果
以写一个加法的函数为例子
add eax,ecx ret
这里的参数指的是就是eax
和ecx
,返回值就是eax
,如下
运行结果后,eax
应该为7
,同时指针回到0040ef44
,运行后观察结果。
如果在参数很多的情况下,计数器可能不够用情况,此时就可以用堆栈进行传递参数。
这里以计算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
寻址。
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
是栈底指针。可以看下面的例子
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
寻址还是很有优势的。
有条件修改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
指令都会使用到此指令sub
,但是不把值进行存储)and
,也不存储数值)SF
,Sigh flag
,有符号整数的最高有效位,0代表为正,1代表为负OF
,Overflow flag
,有符号数加减运算所得结果是否溢出,溢出为1,反之为0of
,无符号数看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