0%

normal题的两个安卓逆向题所学及其拓展

C语言中函数调用约定

首先定义如下函数 int function(int a, int,b)

我们都知道,机器是无法识别代码的,它只认识0,1。我们只需要通过another_number = function(a,b)就能够条用该函数。而在cpu中,计算机并不知道调用该函数需要传递几个、什么类型的参数。为了完成调用这一过程,计算机提供了“栈”这种数据结构来支持传递参数。

(栈的结构等相关内容不再赘述)

函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改栈,使栈恢复原装(初始状态)。

此处面临两个问题:

  • 参数多于1个时,应该按什么顺序把参数压入栈?

  • 函数调用后,由谁来把栈恢复原装?

在高级语言中,通过函数调用约定来说明这两个问题。常见的调用约定有:

  • stdcall
  • cdecl
  • fastcall
  • thiscall
  • naked call

stdcall(很多时候被称为pascal调用约定)

(Win32 API函数绝大部分都是采用_stdcall调用约定的。WINAPI其实也只是_stdcall的一个别名而已。)

stdcall的调用约定意味着:1. 参数从右向左压入栈;2. 函数自身修改栈;3. 函数名自动加”_”,后面紧跟一个”@”符号,其后紧跟着参数的尺寸(字节数)。

在编译时,这个函数的名字按照如上所述,会被翻译成_function@8

函数调用过程的汇编语言如下:

1
2
3
push b      第二个参数入栈
push a 第一个参数入栈
call function 调用参数,注意此时自动把cs:eip入栈

而对于函数自身,则可以翻译为:

1
2
3
4
5
6
7
push ebp     保存ebp寄存器,该寄存器将用来保存栈的栈顶指针,可以在函数退出时恢复
mov ebp,esp 保存栈指针
mov eax,[ebp + 8H] 栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0CH] 栈中ebp + 12处保存了b
mov esp,ebp 恢复esp
pop ebp
ret 8

栈帧

(贴一张栈帧帮助理解)

函数结束后,ret 8表示清理8个字节的栈,函数自己恢复了栈。

cdecl调用约定(C调用约定)

该约定为C语言默认的调用约定:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。命名规则为在函数名前自动加“_”.

被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。

1
int (__cdecl)function(int a,int b)   //加不加_cdecl都一样,只是有没有明确指出的区别

因为是手动清栈,所以在最后的ret便不会清理,故无字节数。

函数自身汇编语言如下,

1
2
3
4
5
6
7
push ebp     保存ebp寄存器,该寄存器将用来保存栈的栈顶指针,可以在函数退出时恢复
mov ebp,esp 保存栈指针
mov eax,[ebp + 8H] 栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0CH] 栈中ebp + 12处保存了b
mov esp,ebp 恢复esp
pop ebp
ret 注意,这里没有修改栈

fastcall

改调用与stdcall类似:

  • 函数的第一个和第二个DWORD参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过从右向左的顺序压栈
  • 被调用函数清理栈
  • 函数名修改规则同stdcall

thiscall

thiscall是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。它是C++类成员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理,thiscall意味着:

  • 参数从右向左入栈
  • 如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入栈。
  • 对参数个数不定的,调用者清理栈,否则函数自己清理栈

(由于此调用约定仅适用于 C++,因此它没有 C 名称修饰方案。)

vectorcall

vectorcall 继承于fastcall 但对于fastcall中的整数仍然按照fastcall规则传递 而浮点以及向量将通过寄存器传递

nakedcall

编译器不会给这种函数增加初始化和清理代码,更特殊的是,你不能用return返回返回值,只能用插入汇编返回结果。这一般用于实模式驱动程序设计。

__pascal

唯一一个从左到右入栈的约定

函数调用约定导致的常见问题

如果定义的约定和使用的约定不一致,则将导致栈被破坏,导致严重问题,下面是两种常见的问题:

  1. 函数原型声明和函数体定义不一致
  2. DLL导入函数时声明了不同的函数约定(所以不能口是心非,会出大问题。)

贴一个微软的对此的解释函数调用约定

再贴一篇关于从汇编语言角度对参数传递的理解的博客 从汇编角度看函数参数传递—汇编函数参数

Java相关

native关键字

native 用来修饰方法,用 native 声明的方法表示告知 JVM 调用,该方法在外部定义,我们可以用任何语言去实现它。 简单地讲,一个native方法就是一个 Java 调用非 Java 代码的接口。

native 语法:

  ①、修饰方法的位置必须在返回类型之前,和其余的方法控制符前后关系不受限制。

  ②、不能用 abstract 修饰,也没有方法体,也没有左右大括号。

  ③、返回值可以是任意类型

System.LoadLibrary

该函数的作用:将指定的模块加载到调用进程的地址空间中。指定的模块可能会导致其他模块被加载。对于其他加载选项,请使用 LoadLibraryEx函数。Ex(extra-额外的)—-总的来说,装载库文件。与之类似的还有System.load

下面对这两个相似的函数做个解析:

  1. 它们都可以用来装载库文件,不论是JNI库文件还是非JNI库文件。在任何本地方法被调用之前必须先用这个两个方法之一把相应的JNI库文件装载。
  2. System.load 参数为库文件的绝对路径,可以是任意路径。
    例如你可以这样载入一个windows平台下JNI库文件:
    System.load(“C://Documents and Settings//TestJNI.dll”);。
  3. System.loadLibrary 参数为库文件名,不包含库文件的扩展名。
    例如你可以这样载入一个windows平台下JNI库文件
    System. loadLibrary (“TestJNI”);

Biginteger

在Java中,由CPU原生提供的整型最大范围是64位long型整数。使用long型整数可以直接通过CPU指令进行计算,速度非常快。

如果我们使用的整数范围超过了long型怎么办?这个时候,就只能用软件来模拟一个大整数。java.math.BigInteger就是用来表示任意大小的整数。BigInteger内部用一个int[]数组来模拟一个非常大的整数

此处仅作简单介绍,详细了解参考如下的教程网站Bitinteger