# X86 ARM 下的函数调用规则

## 序

刚学计机的第一年，虽然 “桟” 这个词经常从老师嘴里冒出，其实他们都没有真正解释过这个东西到底是什么，当然对于大部分学上层的人来说，不知道是无所谓，特别是如果学 Java ，基本上接触不到这个概念。

这篇文章呢就以桟展开，然后说说函数之前的参数是怎么配合桟来实现的，看完这篇文章，无论是在 x86 还是 ARM 架构下面，都应该可以写出汇编和C语言的混合调用的代码，这也是在内核实现中不可缺少的一部分。

## 桟的本质

桟，就是一块可用的内存，它只是规定了数据拿出和放进的顺序，这个顺序就是我们经常说的，先进后出，在上课的时候老师总会用一个桶来比喻，先放进去的东西总是在下面，所以得把后面放的东西拿出来，才能拿最早放进去的。

其实这个顺序为什么是这样，根本不重要，关键在于什么呢，数据是有序放进去的，也就是说拿出来的时候一定也是有序的，它跟数组没有区别，只是我们要拿到第一个元素的时候，是到最后面去拿。但是，**实际上我们也不一定遵循这个顺序**，有时候我们甚至可以直接抛弃一块连续的空间，比如后面抛弃局部变量空间的时候。

在 x86 的体系下，有一个专门的寄存器叫，sp( stack pointer )，它存储了一个地址，就是桟顶的地址，更准确的说，是**最后一个放进去的数据**的地址

```
sp[0] = 最后一个放进去的元素
sp[1] = 倒数第二个放进去的元素
...
```

在初始化的时候，无论是操作系统的初始化，又或是普通程序初始化，其中要做一件事是，分配一块区域，让 sp 指向区域最末端

```c
void * ptr = malloc(4KB);
sp  =  (int)ptr + (1 << 22);

// sp = 0x400000, 假设分配的区域就是 0～0x3fffff

那么 sp[0] = ? 
！注意 这里会造成内存越界

我们说了，sp[0] 指向的是最后一个放进去的数据，现在放数据进去了吗？
很简单的道理， sp[0] 访问的是 0x400000~0x400003 这4 字节
这里假设机器是 32bit

而一块区域并不在我们分配的区域里面
int i = 1;
int * s_sp = (int *）sp；
s_sp--;
*s_sp = 1;
sp = (int)s_sp;

注意这里把 sp 看成是一个整形的变量，实际上
也是如此，指针这个概念只有C才存在
上面代码等同于

push $1  // 把立即数 1 入栈

也等同于 

sub $4, %sp  // sp = sp - 4;
mov $1, (%sp)

此时的 sp == 0x3fffffc
```

上面的代码说明了 sp 永远指向的地方都是存放数据的地方，我们用 \[sp] 访问的是存在数据的地方，当我们想加入新数据的话，也就是 push 操作，**必须得把 sp -= 4** 也就让它的地址指向一块当前没有数据的地方，然后在放入数据，这样 sp 再次指向了**最后一次放入的数据**。

![push](/files/-L_zKk3Q9sDcVWKDtCUR)

那么同理也可以知道，如果执行 pop 操作

{% hint style="info" %}
sp 和 esp 的意义是一样的，读者可以暂时不要区分
{% endhint %}

```c
pop():
    int * p = (int *)sp;
    int data = *p;
    sp = sp - 4; // or  sp = (int)(--p);

pop:
    movl (%esp), %reg
    subl $4, %esp;
<=>
    popl %reg
```

也就是说，先取数据在改变 sp

**pop** 之后，并不会请空无效数据(data)的值，stack pointer已经标示了当前有效数据就是在它之上的数据 其余数据是什么并不关心，所以在C中，一些局部变量，我们虽然在函数体之外，仍然可以访问到。

![pop](/files/-LK-VVULPf-ik4teWYpH)

最后总结一下，栈就是指数据取出和放入遵循**一定规则**的区域，没有什么神秘的地方。然后，sp 指针指向的是最后一个放入数据，如果没有放入数据就访问，会导致内存越界。

## 栈的作用

在理解栈这个东西之后，我们就可以来阐述它存在的意义了。

在程序中，我们会用到很多的变量，也有很多的函数调用，其中绝大多数的变量都是局部变量，这些变量的存在周期只存在于函数调用期间，我们把这种变量称之为局部变量，接下来我们了解到，函数调用是怎么实现返回的，以及为什么局部变量只会在函数内有效，还有函数调用之间的参数如何传递。

### 局部变量

变量，就是一块内存空间，大小取决于它的数据类型本身的大小，而局部变量，其实就是它所在的空间正好就是桟里面。

```c
void foo()
{
    int i=0,j=1,k=2;
    i--;
    j++;
    k--;
    ...
    ...
    return; 
}
 ​
foo:
    push $0    
    push $1
    push $2
    decl (%esp+8)
    incl (%esp+4)
    decl (%esp)
    ...
    ...     
    addl $12, %esp  // 此时桟顶数据已经不再包括i,j,k了
    ret          // 函数返回
```

代码不难理解，下面就是汇编代码，为什么局部变量在函数体之外就不能访问呢，原因很简单，当我们函数返回之前，分配给局部变量的空间我们回收了，回收是通过改变桟指针，sp，来实现的（注意我们的桟是从高地址到低地址），之前也提到了，sp指向的空间以下的空间都是无效的数据，所以我们认为 i,j,k 这几个变量已经无效了，所以它们被称为局部变量。

所谓局部变量，只是桟的一块空间，当我们离开函数之后，桟顶指针就不在指向它们了，读者可能会发现，其实离开函数之后，用 \[sp-4] 不就可以访问了，对的，所以我们说这不是严格意义上的回收，但是访问局部变量是有自己的**规范**的。

### 函数参数传递

函数的参数和局部变量是等价的，当我们调用一个函数之前，就把它的参数放在了桟里，让调用的函数可以使用。

```c
 ​
void bar(int ​i, ​int ​j, ​int ​k);

void foo()
{
    int i=0,j=1,k=2;
    ...
    bar(i,j,k);
    ...
    ...
    return; 
}

foo:
// 局部变量的入桟
    push $0    
    push $1
    push $2
    ...
// 参数入桟 拷贝了一遍局部变量 
    movl (%esp), %reg  
    push %reg  //k入桟
    movl (%esp+4), %reg
    push %reg //j入桟
    mov (%sp+8), %reg
    push %reg //i入桟
// 调用函数
    call bar
    ...
```

所以说局部变量的本质和传递的参数都一样，它们都是**桟上的空间**。有趣的一个地方是，参数是**从右至左**的顺序入桟。其实就是一个习惯，稍后会解释。

上面的代码很清楚解释了一个道理，就是为什么函数里面修改传递来的参数对调用方是没有影响的，因为在调用之间，***要传递的参数都做了拷贝，被调用的函数只是对拷贝的那一份做了修改，并不改变原来的那一份***。有人说引用可以，其实**引用本质就是指针**。

### 函数调用的本质

**函数就是一块代码的首地址，和我们C语言使用的 goto 是没有区别的**。

这是必须得明白的一件事，当我们函数返回的时候，就是 goto 到了原来调用它的地方的下一条代码。那么调用之前，我们得做的事情有什么呢，首先是调用的参数得入桟，然后把函数返回需要到的地方的地址入桟。接着，跳转到目标函数的地址执行。

```c
void bar(int i) {
    ...
}

void foo() {
    ...
    bar(2);
    ...
}

bar:
    0xb00    ...
    ....
    0xbef    ret

foo:
    0xa00    ...
    0xa04    push $2
    0xa08    call bar
    0xa0c    ...

上述的意思就是 foo 的函数代码所在地址 0xa00 
同理  bar 在 0xb00

那么 call 到底做了什么事情
首先  push $0xa0c  也就是返回的时候要执行跳转到的地方（ goto ）
然后  jmp   0xb00    // goto bar
即直接 goto 目标代码所在地方执行

ret 呢
    应该不难猜出 应为已经知道要回到哪个地方了
    首先在 ret 之前得保证 sp 跟进来的时候指向的是同一个地方

明确一点， 栈 是 所 有 函 数 公 用 的， bar函数也会使用 sp
那么如何保证它一定回到最初的位置呢，答案是做备份

下面介绍一个新的寄存器 bp 
在所有函数入口和出口都有这样类似的指令

function:
    pushl   %ebp        // bp 也是全局的 要备份
    movl    %esp, %ebp  // bp = sp   
    ...
    ...    中间局部变量的存在导致 sp 必然会改变
    ...
    movl    %ebp, %esp    // 让 sp = old_sp， 复原
    popl    %ebp        // bp = old_bp
    ret
```

也就是说，bp 寄存器的作用就是备份函数进入时候的栈顶指针，**然后 bp 本身放进了栈里面备份**。然后无论我们函数中间 sp 怎么变，我们都可以保证最后它一定是进来时的位置。

```c
了解了 bp 和 sp 的备份机制
ret 的作用现在继续

刚才说了，调用函数之前除了要将函数要的参数入栈,
call 还会把调用函数的下一条指令入栈
这一步是call 指令自动完成的

所以 ret 之前，我们恢复了 sp 最开始的位置

sp 指向的是 最 后 一 个 入 栈 的 元 素
即调用函数前，最后一个 自动入栈的  下 一 条 指 令 地  址

ret::
    popl  %reg    // 把地址拿出来
    jmp  %reg    // goto 到目标地址

    以上面的例子， reg  = 0xa0c
```

***call*** 和 ***ret*** 做的事情，利用无条件跳转就可以轻松实现对吧，关键得理解，**函数地址就是一个代码的起始地址，每一条指令都是有地址的**。

现在，我们又要介绍一个新的寄存器，它储存的地址就是 将要执行的代码的地址

```c
foo:
    0xa00    ...
    0xa04    push $2
    0xa08    call bar
    0xa0c    ...

    当 CPU 执行 0xa04 指令的时候
    还有一个 ip ( instruct pointer ) 寄存器，指向的就是 0xa08
    我们上课会学到的 PC 指针，它就是本尊啦

也就说 call 其实是这么做的
    pushl  %eip
    jmp    0xxx  目标代码的地址
```

这些寄存器是什么都不重要，关键我们得理解其中栈的作用，就是作为一个**备份的区域**，存函数参数，放了调用函数之后该回到的地方 以及 函数本身要使用的局部变量。

接下来 以下一个例子 来一个总结

```c
int func(int arg1) {
    int local;
    arg1++;
    return arg1;
}

void caller() {
    int ret;
    ret = func(1);
}

caller:
    // 准备阶段
    pushl   %ebp
    movl    %esp, %ebp

    // 开辟 ret 局部变量的空间
    subl    $4, %esp

    // 参数入栈
    pushl   $1
    call   func

    //  存储返回值到本地变量中
    movl    %eax, [sp+4]

   //  还原状态
    movl    %ebp, %esp
    popl    %ebp
    ret
```

![stack](/files/-LQBvJFUvBzo-a4ZIKMZ)

调用 **函数之前 和 函数之后 的 sp 一定是一样**的，所以 sp 指向的一定是最后一次 push 导致 sp 下移的位置。

还有一些前面没有说过的东西，返回值是怎么实现的？就是通过一个 ax 的寄存器，然后发现了吗，我们直接用 ***mov*** 指令来索引本地变量，再次说明，栈只是**遵循一个特定顺序的存储空间**，甚至，假设还有一个本地变量，我们还可以，用

```
mov  reg, [sp+4]
```

之间我们说的，**入栈出栈的顺序根本没有遵守，它就是一块可用的空间。**

```c
int func(int arg1) {
    int local;
    arg1++;
    return arg1;
}

func:
    // 准备阶段
    pushl   %ebp
    movl    %esp, %ebp

   // 开辟 local 局部变量的空间
    subl    $4, %esp

   //  [bp] 是 bp的备份， [bp+4]  是函数返回地址
    incl  (%ebp+8) 

    movl  %(ebp+8), %eax   // 返回值

    //  还原状态
    movl    %ebp, %esp
    popl    %ebp
    ret
```

很明显啦， **bp 用来访问传入的参数，sp 用来访问局部变量，但是它们都是栈上的变量**。

![stack](/files/-LQBxtFgf0QuRkst7Rpd)

传入 arg1 = 1 即是 bp + 8 指向的位置

## x86 arm 的区别

前面说的参数的传入以及局部变量的储存，都是 x86 上的函数调用规则，是有一套协议的，下面给上具体的反汇编代码了，之前的都是伪代码。

```c
int a[5] = {1,2,3,4,5};    /* 数组， 全局变量 */

int  func(int a) {
    return a;
}

int  d(int * a, int b, int c, int d, int e, int f) {
    (*a)++;
    func(b);    
    return e+f;
}

int main(void) {

    d(a, 1,2,3,4, 5);

    return 0;
}
```

![1](/files/-LfhoaIc4Dwp_z_I7P0b)

![2](/files/-La5zX87wcH1y4tqSwfC)

![3](/files/-La60YtYWrYImXcPDr_D)

![4](/files/-LfhnuWVYrRv603fzTyg)

![5](/files/-La60zJOdXZF1UkdzF9W)

ARM 就不一个一个分析了，关键在于参数直接用寄存器和桟传递的，然后返回值也是如此，然后局部变量的位置有所不同，其实就是**函数调用的规范**不一样。

```c
.data
a:
    .word    1
    .word    2
    .word    3
    .word    4
    .word    5

    .text
func:
    str    fp, [sp, #-4]!
    add    fp, sp, #0
    sub    sp, sp, #12
    str    r0, [fp, #-8]
    ldr    r3, [fp, #-8]
    mov    r0, r3
    add    sp, fp, #0

    ldr    fp, [sp], #4
    bx    lr

d:
    push    {fp, lr}
    add    fp, sp, #4
    sub    sp, sp, #16
    str    r0, [fp, #-8]
    str    r1, [fp, #-12]
    str    r2, [fp, #-16]
    str    r3, [fp, #-20]
    ldr    r3, [fp, #-8]
    ldr    r3, [r3]
    add    r2, r3, #1
    ldr    r3, [fp, #-8]
    str    r2, [r3]
    ldr    r0, [fp, #-12]
    bl    func
    ldr    r2, [fp, #4]
    ldr    r3, [fp, #8]
    add    r3, r2, r3
    mov    r0, r3
    sub    sp, fp, #4

    pop    {fp, lr}
    bx    lr


main:
    push    {fp, lr}
    add    fp, sp, #4
    sub    sp, sp, #8
    mov    r3, #5
    str    r3, [sp, #4]
    mov    r3, #4
    str    r3, [sp]
    mov    r3, #3
    mov    r2, #2
    mov    r1, #1
    ldr    r0, .L7
    bl    d
    mov    r3, #0
    mov    r0, r3
    sub    sp, fp, #4

    pop    {fp, lr}
    bx    lr

.L7:
    .word    a
```

给出调用的桟

![](/files/-LfhvjtIxjdzoNHQMhi-)

在x86当中，默认的**push**指令就是**Decreasing Before**模式，但是在Arm中没有原生，对于**push, pop**的指令，实际上还是会转换到**ldr，str**或者它的衍生指令。之所以出现STMDA的栈指令，就是为了操作方便，比如你存进去栈的时候是 Full Descending 那么取出来的时候就是 Empty Ascending

![form Net](/files/-Lfhv5BTYCmomSatv9Ir)

## 总结

到如此，我的耐心已经消耗殆尽拉0.0，给出俩张图，读者自己好好理解，所以我很佩服热衷分享的人，并且还能说的很详细的人！

x86

![](/files/-LfhpnV9ex-NszHYNh09)

ARM

![](/files/-LJWT_QmOqFrzo1bXW4a)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://trance.gitbook.io/blog/notes/x86-arm-call-convention.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
