4

ARM64下Indirect Result Location摸索

 3 years ago
source link: http://satanwoo.github.io/2017/04/23/ARM64IndirectReturn/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

ARM64下Indirect Result Location摸索

之前学习汇编的时候,大概了解了一些ARM64下寄存器的用途,比如x0 - x7作为函数传递使用。同时,x0也可以作为函数返回值时候的寄存器。

但是,今天在研究一些跟返回结构体相关的时候,发现返回值并不是放在X0寄存器中。上网搜索了一下资料,发现在ARM64下,当一个Callee函数返回的内容大于16bytes的时候,该内容会被存到一个内存地址当中,然后这个内存地址的值会存入寄存器x8。后续Caller函数在使用该返回值的时候,会从X8寄存器中取出内存地址,并从内存地址取出内容的值

是不是有点绕,还是让我们来看个例子吧。

首先我根据大于16 bytes的要求定义了如下结构体:

typedef struct {
    int64_t i;
    int64_t j;
    int64_t k;
} MYStruct;

在ARM64下,该结构体默认按4 bytes对齐,每个int64占用8 bytes,因此结构体大小24 bytes

我们定义如下函数,用于返回一个该结构体:

- (MYStruct)testIndirectResultLocation:(int64_t)i1 second:(int64_t)i2 th:(int64_t)i3
{
    MYStruct s;
    s.i = i1;
    s.j = i2;
    s.k = i3;
    return s;
}

这个函数很简单,传入三个值。然后构造个局部变量MYStruct s,将其对应的成员变量按照刚刚的传入参数赋值,最后返回该结构体。

该函数调用在未优化的前提下的汇编结果如下:

IndirectResultLocation`-[ViewController testIndirectResultLocation:second:th:]:
    // 预留空间
    <+0>:  sub    sp, sp, #0x40             ; =0x40 

    // 存参
    <+4>:  str    x0, [sp, #0x38]
    <+8>:  str    x1, [sp, #0x30]
    <+12>: str    x2, [sp, #0x28]
    <+16>: str    x3, [sp, #0x20]
    <+20>: str    x4, [sp, #0x18]

    // 赋值
->  <+24>: ldr    x0, [sp, #0x28]
    <+28>: str    x0, [sp]
    <+32>: ldr    x0, [sp, #0x20]
    <+36>: str    x0, [sp, #0x8]
    <+40>: ldr    x0, [sp, #0x18]
    <+44>: str    x0, [sp, #0x10]

    // 将结构体存到x8寄存器的值代表的地址去
    <+48>: ldr    x0, [sp]
    <+52>: str    x0, [x8]
    <+56>: ldr    x0, [sp, #0x8]
    <+60>: str    x0, [x8, #0x8]
    <+64>: ldr    x0, [sp, #0x10]
    <+68>: str    x0, [x8, #0x10]

    // 释放空间
    <+72>: add    sp, sp, #0x40             ; =0x40 
    <+76>: ret    

第一行:SP即Stack Pointer,向下减0x40(64 bytes)的大小,预先分配出函数需要用的栈空间。为什么要预留这么多的大小呢?首先按照Objective-C的函数调用规定,前两个参数必须是selfselector,也即会使用到寄存器X0X1。然后该函数有三个形参,使用了X2-X4的寄存器
上述这五个,大小占用了self(8 bytes) + selector(8 bytes) + 三个参数(24 bytes) = 40 bytes。那么还有24 bytes去干嘛了呢?

别忘了,我们在函数中可以声明了一个局部变量MYStruct s,该结构体大小是24 bytes。而在函数调用中使用到的变量,基本上都在栈区中开辟对应的空间进行暂存。

后续第二行到第六行非常简单易懂,就是把上述5个参数存到实际的栈区中去使用。按照这个存法以后,内存布局如下(注意高地址在上,低地址在下,ARM下的栈是向下增长):

%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202017-04-23%20%E4%B8%8A%E5%8D%882.48.35.png?raw=true

将参数都存入到栈以后,我们就要对结构体进行赋值了,这些操作在第七行到第十二行之间。
1赋值给[SP],2赋值给[SP + #0x8],3赋值给[SP + #0x10]。如果不理解啥意思的话,可以看下我自己转化的伪代码:

void *address = &s;
*(int64_t *)(address) = 1;
*(int64_t *)(address + 8) = 2;
*(int64_t *)(address + 16) = 3;

赋值完以后,我们可以通过内存分布看下数据是否正确:

%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202017-04-23%20%E4%B8%8A%E5%8D%882.30.06.png?raw=true
%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202017-04-23%20%E4%B8%8A%E5%8D%882.29.53.png?raw=true

当赋值完成后,就要进行结构体的返回了。这里不是简单的mov x0, sp之类的操作,而是一串和X8寄存器相关操作。

其实原理差不多,转化成伪代码的话,基本上是这样:

void *toSaveAddress = [x8];
void *valueNowAddress = [sp];

*(int64_t *)(toSaveAddress) = *valueNowAddress;
*(int64_t *)(toSaveAddress + 8) = *(valueNowAddress + 8);
*(int64_t *)(toSaveAddress + 16) = *(valueNowAddress + 16);

操作完成后,释放空间即可。

其实ARM64在汇编层面实现的这么复杂, 我们在编程层面只要按照如下方式理解即可:

some_struct foo(int arg1, int arg2);
some_struct s = foo(1, 2);

会被编译成:

some_struct* foo(some_struct* ret_val, int arg1, int arg2);
some_struct s; 
foo(&s, 1, 2);

从本文中我们不难看出,ARM64针对不同大小的返回值都有着对应的Calling Convention。下次我准备来摸索下,处于8 bytes - 16 bytes之间的返回值究竟是怎么处理的。


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK