话不多说,直接开干!!!
案例一:
首先实现一个最简单的c语言代码,hello world。
#include <stdio.h>
void func(){
printf("hello world\n");
}
int main(){
func();
return 0;
}
使用编译指令进行生成汇编代码:
-O0
gcc -S -O0 test.c
.arch armv8-a
.file "test.c"
.text
.section .rodata
.align 3
//上面这段代码的含义是:指定当前源文件的名称为test.c; .text表示后续的这段指令将属于代码段; .section .rodata表示将定义一个名为 .rodata 的段,它通常用于存储只读数据(read-only data)。在这里,.rodata 可能包含程序中使用但不修改的常量数据;.align 3表示将 .rodata 段的起始地址对齐到2的3次方(即8字节)的边界。这样的对齐有助于提高内存访问的效率,因为许多硬件架构要求数据在特定地址上对齐。
.LC0:
.string "hello world"
.text
.align 2
.global func
.type func, %function
//上面这段代码表示:首先定义一个字符串hello world,字节对齐为2的2次方位置;定义一个全局符号func,并表明该func的类型是一个函数。
func:
stp x29, x30, [sp, -16]!
//由于main函数调用了func函数,所以这里保存main的相关数据和指令。具体是:x29保存main的指向自己的局部变量的一块内存的地址,x30保存的是main函数后续指令的地址,以保证func函数返回后能够继续运行main函数。然后将x29,和x30寄存器放入栈中(sp表示栈),因为栈是从搞地址向低地址增长的,所以入栈是减16.
add x29, sp, 0
//把栈sp中的数据复制到寄存器x29中,确保当前函数能够调用局部变量。
adrp x0, .LC0
//adrp=adress of page 表示将.LC0表示的数据所在的页加载到寄存器x0.
add x0, x0, :lo12:.LC0
//将寄存器x0指向的页数据加上偏移量lo12,表示获取.LO0的数据放到寄存器x0.
bl puts
//bl表示调用函数,并返回,调用的函数是puts函数,也就是printf函数。
nop
ldp x29, x30, [sp], 16
//回复调用者的局部变量内存和指令地址到寄存器x29和x30,并恢复栈sp。
ret
.size func, .-func
.align 2
.global main
.type main, %function
//
main:
stp x29, x30, [sp, -16]!
add x29, sp, 0
bl func
mov w0, 0
//将常数值 0 移动到通用寄存器 w0 中。在 ARM64 中,通用寄存器 w0 是用于传递返回值的寄存器。
ldp x29, x30, [sp], 16
ret
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
-O3
gcc -S -O3 test.c
.arch armv8-a
.file "test.c"
.text
.align 2
.p2align 3,,7
.global func
.type func, %function
func:
adrp x0, .LC0
add x0, x0, :lo12:.LC0
b puts
.size func, .-func
.section .text.startup,"ax",@progbits
.align 2
.p2align 3,,7
.global main
.type main, %function
main:
stp x29, x30, [sp, -16]!
adrp x0, .LC0
add x0, x0, :lo12:.LC0
add x29, sp, 0
bl puts
mov w0, 0
ldp x29, x30, [sp], 16
ret
.size main, .-main
.section .rodata.str1.8,"aMS",@progbits,1
.align 3
.LC0:
.string "hello world"
.ident "GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
总结:
就目前了解的来看,主要是分为几个步骤。首先是将局部变量内存和后续指令保存到栈中,然后进行当前函数的计算,然后返回到调用函数。其中数据的加载分为了两步,先是加载了数据所在的内存页的地址,然后使用偏移量加载该页中的具体数据。
arm64中的寄存器由32个,即x0-x31,一个是64bit,也就是8byte,所以一次入栈两个寄存器改变的是16个字节。
在开启-O3级别的优化时,最大的变化是func函数没有了调用者局部变量和后续指令的入栈和出栈指令,并且puts函数的调用只是b puts,不是bl puts了,表示func函数调用结束没有返回了。
b
是无条件分支指令,它直接跳转到目标地址执行,不保存返回地址。用于无条件地改变程序的执行流程。bl
是带链接的分支指令,它不仅跳转到目标地址执行,还将下一条指令的地址保存到链接寄存器 lr
(Link Register)中。这对于函数调用是非常有用的,因为函数执行完毕后可以通过 ret
指令返回到调用者。
所以-O3优化就是减少了多余的、没有必要的入栈出栈操作,以及调用返回的指令跳转操作。