我们知道C言语要运行的话是需要用到栈的,每次函数调用都会对栈进行生长和回退。本函数生长的栈用于本函数内数据存储,当本函数返回时需要回退栈,将栈恢复成调用者的现场继续运行。
我们在aarch64体系结构上来观察一下C语言的这个调用和返回过程:
C言语代码(为了示范我们进行两级函数调用,main->swap1->swap):
#include <stdio.h>
static int swap(int *a1, int *a2)
{
int tmp;
if ((NULL == a1) || (NULL == a2))
{
return -1;
}
tmp = *a1;
*a1 = *a2;
*a2 = tmp;
return 0;
}
static int swap1(int *a1, int *a2)
{
int *p1 = a1;
int *p2 = a2;
int ret;
ret = swap(p1, p2);
return ret;
}
int main()
{
int a = 1;
int b = 2;
int ret;
ret = swap1(&a, &b);
if (ret)
{
return -1;
}
printf("a = %d, b = %d\n", a, b);
return 0;
}
将上述C代码通过gcc -S file.c -o file.s命令生成汇编代码。
.arch armv8-a
.file "swap.c"
.text
.align 2
.type swap, %function
swap:
sub sp, sp, #32 //将sp下移32字节
str x0, [sp, 8] //将第一个参数p1存到sp+8的地方
str x1, [sp] //将第二个参数p2存到sp+0的地方
ldr x0, [sp, 8] //将sp+8处所存的数据放到x0中,x0=p1
cmp x0, 0 //用p1和0比较
beq .L2 //如果p1和0相等则跳到L2处返回-1
ldr x0, [sp] //x0=p1
cmp x0, 0 //用p2和0比较
bne .L3 //如果p2不等于0则跳到L3处执行交换数据代码,否则自然进入L2返回-1
.L2:
mov w0, -1
b .L4
.L3:
ldr x0, [sp, 8]
ldr w0, [x0] //这两句完成w0=*p1
str w0, [sp, 28] //将*p1存到sp+28的地方,tmp=*p1
ldr x0, [sp]
ldr w1, [x0] //这两句完成w1=*p2
ldr x0, [sp, 8] // x0=p1
str w1, [x0] // *p1=*p2
ldr x0, [sp] // x0=p2
ldr w1, [sp, 28] // w1=tmp
str w1, [x0] // *p2=tmp
mov w0, 0 //执行成功将返回值置为0
.L4:
add sp, sp, 32 //恢复x29和lr的值,并将sp上移32个字节,销毁本次函数swap1使用的栈
ret //返回
.size swap, .-swap
.align 2
.type swap1, %function
swap1:
stp x29, x30, [sp, -64]! //将本函数的栈底指针x29和返回地址lr保存到栈中,并将栈顶指针sp向下移64个字节,完成开辟本函数需要使用的栈
add x29, sp, 0 //更新栈底指针,此时x29 = sp
str x0, [x29, 24] //将第一个参数a1存到sp+24的地方,占8个字节
str x1, [x29, 16] //将第二个参数a2存到sp+16的地方,占8个字节
ldr x0, [x29, 24] //将sp+24处所存的数据放到x0中
str x0, [x29, 56] //将x0中的数据存入sp+56的地方,上面一句和这一句完成C代码中的p1=a1
ldr x0, [x29, 16] //将sp+16处所存的数据放到x0中
str x0, [x29, 48] //将x0中的数据存入sp+48的地方,完成C代码中的p1=a2
ldr x1, [x29, 48] //准备调用下级函数的第二个参数x1,将sp+48处存的数据放到x1中,x1=p2
ldr x0, [x29, 56] //准备调用下级函数的第一个参数x0,x0=p1
bl swap //调用swap函数,参数为p1和p2
str w0, [x29, 44] //将返回值存入sp+44的地方
ldr w0, [x29, 44] //将sp+44处存放的数据放到w0中作为返回值,这两句没有也可以,因为这两句之间w0的值没有人改过
ldp x29, x30, [sp], 64 //恢复x29和lr的值,并将sp上移64个字节,销毁本次函数swap1使用的栈
ret //返回
.size swap1, .-swap1
.section .rodata
.align 3
.LC0:
.string "a = %d, b = %d\n"
.text
.align 2
.global main
.type main, %function
main:
stp x29, x30, [sp, -32]!
add x29, sp, 0
mov w0, 1
str w0, [x29, 24]
mov w0, 2
str w0, [x29, 20]
add x1, x29, 20
add x0, x29, 24
bl swap1
str w0, [x29, 28]
ldr w0, [x29, 28]
cmp w0, 0
beq .L8
mov w0, -1
b .L10
.L8:
ldr w1, [x29, 24]
ldr w2, [x29, 20]
adrp x0, .LC0
add x0, x0, :lo12:.LC0
bl printf
mov w0, 0
.L10:
ldp x29, x30, [sp], 32
ret
.size main, .-main
.ident "GCC: (Debian 6.3.0-18+deb9u1) 6.3.0 20170516"
.section .note.GNU-stack,"",@progbits
上面的汇编代码中对swap1和swap函数进行了注释,我们可以看到这两个函数都使用了栈对寄存器或者中间运算数据进行存储,那么下面我们将上述汇编进行修改一下,改为swap1和swap函数不使用栈的方式:
.arch armv8-a
.file "swap.c"
.text
.align 2
.type swap, %function
swap:
cmp x0, 0
beq .L2 //第一个参数判空
cmp x0, 0
bne .L3 //第二个参数判空
.L2:
mov w0, -1
b .L4
.L3:
ldr w2, [x0] // tmp1=*p1
ldr w3, [x1] // tmp2=*p2
str w3, [x0] // *p1 = tmp2
str w2, [x1] // *p2 = tmp1
mov w0, 0 //完成值交换,置返回值为0
.L4:
ret //本函数中我们没有修改lr也没有修改上级函数用的x6所以可以不需要恢复任何现场直接返回
.size swap, .-swap
.align 2
.type swap1, %function
swap1:
mov x6, x30 //只保存lr到x6寄存器中
mov x7, x0 //将第一个参数放到x7中
mov x8, x1 //将第二个参数放到x8中
mov x0, x7 //又将第一个参数从x7中放到x0中
mov x1, x8 //又将第二个参数从x8中放到x1中,其它这几句话没有意义,只是为了模仿上面的C代码,我们可以看到这几句话没有修改使用其它值修改x0和x1所以可以直接什么都不做,将参数透传给swap函数
bl swap //调用swap函数,参数放在x0和x1中
mov x30, x6 //从x6中恢复lr,注意:我们在本级函数使用了x6保存数据,那么下级函数如果要使用x6则需要保存x6的现场和恢复x6后才能返回到本级函数。
ret
.size swap1, .-swap1
.section .rodata
.align 3
.LC0:
.string "a = %d, b = %d\n"
.text
.align 2
.global main
.type main, %function
main:
stp x29, x30, [sp, -32]!
add x29, sp, 0
mov w0, 1
str w0, [x29, 24]
mov w0, 2
str w0, [x29, 20]
add x1, x29, 20
add x0, x29, 24
bl swap1
str w0, [x29, 28]
ldr w0, [x29, 28]
cmp w0, 0
beq .L8
mov w0, -1
b .L10
.L8:
ldr w1, [x29, 24]
ldr w2, [x29, 20]
adrp x0, .LC0
add x0, x0, :lo12:.LC0
bl printf
mov w0, 0
.L10:
ldp x29, x30, [sp], 32
ret
.size main, .-main
.ident "GCC: (Debian 6.3.0-18+deb9u1) 6.3.0 20170516"
.section .note.GNU-stack,"",@progbits
从修改后的汇编代码中我们可以看出,swap1和swap两个函数中没有使用栈,没有动过sp和x29寄存器,也同样完成了数据交换,达到了C代码想要完成的目的。怎么做到的呢?其实只是将之前需要保存到栈中的寄存器和数据保存到了另外的寄存器中,相当于是把寄存器当成了临时存储器来使用。所以不用栈汇编语言也可以运行。