iOS逆向:函数本质(下)

2021-05-05  本文已影响0人  iOS鑫

本文主要是讲解函数的参数返回值局部变量在汇编中是如何存储,以及CPSR标志寄存器

函数的参数和返回值

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130595548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

查看系统的参数汇编

下面通过系统中对函数的汇编来查看系统对参数、返回值是如何操作的

int sum(int a, int b){
    return a + b;
}
- (void)viewDidLoad{
    [super viewDidLoad];
    sum(10, 20);
}

自己优化实现suma

运行发现,其结果与sum函数是一致的,结果都是30

<!--asm.s-->
.text
.global _suma

_suma:
    add x0, x0, x1
    ret 

<!--调用-->
int suma;
- (void)viewDidLoad{
    [super viewDidLoad];
    suma(10, 20);
}

编译器优化

来看以下代码的汇编

int test(int a, int b, int c, int d, int e, int f, int g, int h, int i){
    return a + b + c + d + e + f + g + h + i;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    test(1, 2, 3, 4, 5, 6, 7, 8, 9);
}

编译器优化

- (void)viewDidLoad {
    [super viewDidLoad];
    printf("%d", test(1, 2, 3, 4, 5, 6, 7, 8, 9));
}

汇编代码如下,发现优化后的test函数在汇编中,其本质是一个,也就是test函数的返回值。(相当于将printf("%d", test(1, 2, 3, 4, 5, 6, 7, 8, 9));直接优化成了printf("%d", 45);

通过汇编实现函数

int funcA(int a, int b);

- (void)viewDidLoad {
    [super viewDidLoad];
    int a = funcA(10, 20);
    printf("%d", a);
}

.text
.global _funcA, _sum

_funcA:
    sub sp, sp, #0x10
    stp x29, x30, [sp] //保护lr
    bl _sum
    ldp x29, x30, [sp]
    add sp, sp, #0x10
    ret

_sum:
    add x0, x0, x1
    ret

<!--简写-->

_funcA:
    stp x29, x30, [sp, #-0x10]!
    bl _sum
    ldp x29, x30, [sp], #0x10
    ret

_sum:
    add x0, x0, x1
    ret

<!--巧合:还可以将bl替换成b-->
//b就是简单跳转,在逆向中用于绕过某些代码(例如安全监测)

_funcA:
    b _sum

_sum:
    add x0, x0, x1
    ret

运行结果如下所示

说明:

  • 关于b指令:只是跳转,不改变lr寄存器
  • 拉伸栈空间和参数个数有没有关系?:有关系,当参数越多时,如果寄存器放不下,就需要用到内存。就会将栈空间放大,影响栈空间的不仅仅是参数个数,还有局部变量

函数的返回值

如果返回值是一个结构体类型,一个寄存器放不下,这时是什么情况?

有以下代码,运行查看其汇编

struct str {
    int a;
    int b;
    int c;
    int d;
    int f;
    int g;
};
struct Str getStr(int a, int b, int c, int d, int f, int g){
    struct Str str1;
    str1.a = a;
    str1.b = b;
    str1.c = c;
    str1.d = d;
    str1.f = f;
    str1.g = g;
    return str1;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    struct Str str2 = getStr(1, 2, 3, 4, 5, 6);
}

结论:如果返回值大于x0的8个字节,也会使用栈空间来存储

练习

1、如果函数的参数/返回是9个呢?

struct Str{
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
    int g;
    int h;
    int i;
};

struct Str getStr(int a, int b, int c, int d, int e, int f, int g, int h, int i){
    struct Str str1;
    str1.a = a;
    str1.b = b;
    str1.c = c;
    str1.d = d;
    str1.e = e;
    str1.f = f;
    str1.g = g;
    str1.h = h;
    str1.i = i;
    return str1;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    struct Str str2 = getStr(1, 2, 3, 4, 5, 6, 7, 8, 9);
}

2、如果结构体参数是10个呢?

struct Str{
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
    int g;
    int h;
    int i;
    int j;
};

struct Str getStr(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j){
    struct Str str1;
    str1.a = a;
    str1.b = b;
    str1.c = c;
    str1.d = d;
    str1.e = e;
    str1.f = f;
    str1.g = g;
    str1.h = h;
    str1.i = i;
    str1.j = j;
    return str1;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    struct Str str2 = getStr(1, 2, 3, 4, 5, 6, 7, 8, 9);

结论:前8个参数存储到寄存器,后两个参数存储到栈空间

函数的局部变量

分析下面代码的汇编

int funcB(int a, int b){
    int c = 6;
    return a + b + c;
}

- (void)viewDidLoad {
    [super viewDidLoad]; 
    funcB(10, 20);
}

总结

如果函数有嵌套调用的情况呢?

int funcB(int a, int b){
    int c = 6;
    int d = funcSum(a, b, c);
    return d;
}

int funcSum(int a, int b, int c){
    int d = a + b + c;
    printf("%d", d);
    return d;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    funcB(10, 20);
}

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130595548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

标记/状态寄存器

标记/状态寄存器:主要用于控制程序的执行流程

引子

分析下面函数的汇编

void func(){
    int a = 1;
    int b = 2;

    if (a == b){
        printf("a == b");
    }else{
        printf("error");
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];

    func();
}

CPSR

N(Negative)标志

案例调试

查看以下代码的汇编

void func(){
     asm(
        "mov w0, #0xffffffff\n"
        "adds w0, w0, #0x0\n"
        );
}
- (void)viewDidLoad {
    [super viewDidLoad];

    func();
}

Z(zero)标志

对于Z的值,可以这样看,Z标记相关指令的计算结果是否为0,如果为0,则Z要记录下”是0“这样的肯定信息。在计算机中1表示逻辑真,表示肯定,所以当结果为0时Z=1(表示”结果为0“)。如果结果不为0,则Z要记录下”不是0“这样的否定信息。在计算机中0表示逻辑假,表示否定,所以当结果不为0时Z=0,表示”结果不为0“

案例调试

目的:验证z为1时,N必为0

void func(){
    asm(
        "mov w0, #0x0\n"
        "adds w0, w0, #0x0\n"
    );
}

void func(){
    asm(
        "mov w0, #0x0\n"
        "adds w0, w0, #0x1\n"
    );
}

查看CPSR,从图中可以看出,由于为结果为非负数,所以N为0,同时也不为0,则Z为0

C(Carry)标志

*   例如 0000 0001 - 0000 0010 --> 1111 1111,

对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N-1位,就是它的最高有效位,而假想存在的第N位,就是相对于最高有效位的更高位,如下所示

image
进位

当两个数相加时,有可能产生从最高有效位向更高位的进位,例如两个32位数据0xaaaaaaaa + 0xaaaaaaaa,将产生进位,由于这个进位值在32位中无法保存,就说这个进位值丢失了。其实CPU在运算时,并不丢弃这个进位制,而是记录在一个特殊的寄存器的某一位上,ARM下就用C位来记录这个进位值,例如下面的指令

mov w0,#0xaaaaaaaa;0xa 的二进制是 1010
adds w0,w0,w0; 执行后 相当于 1010 << 1 进位1(无符号溢出) 所以C标记 为 1
adds w0,w0,w0; 执行后 相当于 0101 << 1 进位0(无符号没溢出) 所以C标记 为 0
adds w0,w0,w0; 重复上面操作
adds w0,w0,w0

借位

当两个数据做减法时,有可能向更高位借位,例如,两个32位数据0x00000000 - 0x000000ff,将产生借位,借位后,相当于计算0x100000000 - 0x000000ff,得到0xffffff01这个值,由于借了一位,所以C位用来标记借位。C=0,例如下面的指令

mov w0,#0x0
subs w0,w0,#0xff
subs w0,w0,#0xff
subs w0,w0,#0xff

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130595548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

总结

上一篇下一篇

猜你喜欢

热点阅读