持续更新中。。。
一些预备性的知识点
1、平台:孙海勇老师的CLFS 4.0(20220518版)
https://github.com/sunhaiyong1978/CLFS-for-LoongArch/releases/tag/4.0
- 汇编程序:binutils
- 编译程序:gcc
- 二进制调用接口:lp64d
2、通用寄存器,32个,binutils里面的定义:
const char *const loongarch_r_normal_name[32] =
{
"$r0", "$r1", "$r2", "$r3", "$r4", "$r5", "$r6", "$r7",
"$r8", "$r9", "$r10", "$r11", "$r12", "$r13", "$r14", "$r15",
"$r16", "$r17", "$r18", "$r19", "$r20", "$r21", "$r22", "$r23",
"$r24", "$r25", "$r26", "$r27", "$r28", "$r29", "$r30", "$r31",
};
const char *const loongarch_r_lp64_name[32] =
{
"$zero", "$ra", "$tp", "$sp", "$a0", "$a1", "$a2", "$a3",
"$a4", "$a5", "$a6", "$a7", "$t0", "$t1", "$t2", "$t3",
"$t4", "$t5", "$t6", "$t7", "$t8", "$x", "$fp", "$s0",
"$s1", "$s2", "$s3", "$s4", "$s5", "$s6", "$s7", "$s8",
};
3、程序加载到内存的映像
图形界面点击或在命令行终端输入程序名回车的时候,系统会读取文件、分析文件、创建进程、分配空间完成程序加载。
程序加载后,程序的内存映像如图,起始内存区是.text段,也叫代码段,是程序的指令部分。
代码段之后是数据段,是程序的数据部分,其中包括初始化的、未初始化的。
内存映像的高端是栈(stack),是程序运行过程中函数调用过程中的临时存储区
栈和数据段之间的内存空间是堆(heap),也叫自由(free)空间,这段内存空间供程序运行过程中动态分配内存用。
栈空间从高位地址向低位地址增长,而堆空间从地址地位向高位增长。程序正常运行时,两个空间不能重合。
程序加载完成后,处理器的PC寄存器指向main函数的入口,程序开始运行,实际的运行流程可以看函数入口后面的汇编代码。期间可以观察:
- 怎样调用函数
- 怎样传递参数
- 怎样构建栈帧
- 怎样保存临时变量
- 怎样返回数据
- 怎样释放栈帧
- 怎样函数返回
测试用C语言代码和中间文件的生成
代码源自Richard Blum的《Professional Assembly Language》
/* tempconv.c - An example for viewing assembly source code */
#include <stdio.h>
float convert(int deg)
{
float result;
result = (deg - 32.) / 1.8;
return result;
}
int main()
{
int i = 0;
float result;
printf(" Temperature Conversion Chart\n");
printf("Fahrenheit Celsius\n");
for(i = 0; i < 230; i = i + 10)
{
result = convert(i);
printf(" %d %5.2f\n", i, result);
}
return 0;
}
编译器输出的汇编代码和解读
// 这个命令是编译源文件(词法分析、语法语义分析、产生中间代码)然后输出等效的汇编代码
// 在clfs环境下,编译程序默认的arch是loongarch64,abi是lp64d,也可以显示的指定
// 这个命令的输出文件是 tempconv.s ,如果不指定 -S 参数,默认会直接生成可执行文件
gcc -S tempconv.c
简单介绍下LoongArch的汇编代码的一些基本元素:
“.”开头的小写字母字符串,一般是伪指令,大部分是gas程序中定义的,可以看as手册。
字符串以 ”:“ 结尾的,是标签。
“.”开头的大写字母字符串,后面用“:”结尾,是普通标签,比如 .LFE0:
小写字母字符串开头,结尾没有”:“ 的,是指令助记符,后面跟的是参数
汇编代码中,一行是一个语句,as用换行符来判断
LoongArch 支持一行多语句,用”;“ 分开,但一般都不这么写。
指令助记符,可以是处理器的操作码,可以在指令集手册中查到,也可以是宏,参考binutils的代码实现
参数部分,需要结合指令集手册里面的指令格式和代码上下文,明确哪些是寄存器、立即数和地址
.file "tempconv.c"
.text
.align 2
.globl convert
.type convert, @function
convert:
.LFB0 = .
.cfi_startproc
addi.d $r3,$r3,-48
.cfi_def_cfa_offset 48
st.d $r22,$r3,40
.cfi_offset 22, -8
addi.d $r22,$r3,48
.cfi_def_cfa 22, 0
or $r12,$r4,$r0
st.w $r12,$r22,-36
ld.w $r12,$r22,-36
movgr2fr.w $f0,$r12
ffint.d.w $f1,$f0
la.local $r12,.LC0
fld.d $f0,$r12,0
fsub.d $f1,$f1,$f0
la.local $r12,.LC1
fld.d $f0,$r12,0
fdiv.d $f0,$f1,$f0
fcvt.s.d $f0,$f0
fst.s $f0,$r22,-20
fld.s $f0,$r22,-20
ld.d $r22,$r3,40
.cfi_restore 22
addi.d $r3,$r3,48
.cfi_def_cfa_register 3
jr $r1
.cfi_endproc
.LFE0:
.size convert, .-convert
.section .rodata
.align 3
.LC2:
.ascii " Temperature Conversion Chart\000"
.align 3
.LC3:
.ascii "Fahrenheit Celsius\000"
.align 3
.LC4:
.ascii " %d %5.2f\012\000"
.text
.align 2
.globl main
.type main, @function
main:
.LFB1 = .
.cfi_startproc
addi.d $r3,$r3,-32
.cfi_def_cfa_offset 32
st.d $r1,$r3,24
st.d $r22,$r3,16
.cfi_offset 1, -8
.cfi_offset 22, -16
addi.d $r22,$r3,32
.cfi_def_cfa 22, 0
st.w $r0,$r22,-24
la.local $r4,.LC2
bl %plt(puts)
la.local $r4,.LC3
bl %plt(puts)
st.w $r0,$r22,-24
b .L4
.L5:
ldptr.w $r12,$r22,-24
or $r4,$r12,$r0
bl convert
fst.s $f0,$r22,-20
fld.s $f0,$r22,-20
fcvt.d.s $f0,$f0
ldptr.w $r12,$r22,-24
movfr2gr.d $r6,$f0
or $r5,$r12,$r0
la.local $r4,.LC4
bl %plt(printf)
ld.w $r12,$r22,-24
addi.w $r12,$r12,10
st.w $r12,$r22,-24
.L4:
ld.w $r12,$r22,-24
slli.w $r13,$r12,0
addi.w $r12,$r0,229 # 0xe5
ble $r13,$r12,.L5
or $r12,$r0,$r0
or $r4,$r12,$r0
ld.d $r1,$r3,24
.cfi_restore 1
ld.d $r22,$r3,16
.cfi_restore 22
addi.d $r3,$r3,32
.cfi_def_cfa_register 3
jr $r1
.cfi_endproc
.LFE1:
.size main, .-main
.section .rodata
.align 3
.LC0:
.word 0
.word 1077936128
.align 3
.LC1:
.word -858993459
.word 1073532108
.ident "GCC: (GNU) 12.1.0"
.section .note.GNU-stack,"",@progbits
// 下面的内容是使用如下命令产生的
gcc -c tempconv.c // 生成 tempconv.o 二进制文件
objdump -d tempconv.o // 对 tempconv.o 进行反汇编
// 反汇编信息如下
tempconv.o: file format elf64-loongarch
// 和gcc -S 产生的汇编代码指令部分对比的话,会发现一些细微的不同之处
// move 指令和 or 指令是等效的
// 寄存器名使用了lp64d的名称约定,更便于理解
Disassembly of section .text:
0000000000000000 <convert>:
0: 02ff4063 addi.d $sp, $sp, -48(0xfd0)
4: 29c0a076 st.d $fp, $sp, 40(0x28)
8: 02c0c076 addi.d $fp, $sp, 48(0x30)
c: 0015008c move $t0, $a0
10: 29bf72cc st.w $t0, $fp, -36(0xfdc)
14: 28bf72cc ld.w $t0, $fp, -36(0xfdc)
18: 0114a580 movgr2fr.w $fa0, $t0
1c: 011d2001 ffint.d.w $fa1, $fa0
20: 1c00000c pcaddu12i $t0, 0
24: 02c0018c addi.d $t0, $t0, 0
28: 2b800180 fld.d $fa0, $t0, 0
2c: 01030021 fsub.d $fa1, $fa1, $fa0
30: 1c00000c pcaddu12i $t0, 0
34: 02c0018c addi.d $t0, $t0, 0
38: 2b800180 fld.d $fa0, $t0, 0
3c: 01070020 fdiv.d $fa0, $fa1, $fa0
40: 01191800 fcvt.s.d $fa0, $fa0
44: 2b7fb2c0 fst.s $fa0, $fp, -20(0xfec)
48: 2b3fb2c0 fld.s $fa0, $fp, -20(0xfec)
4c: 28c0a076 ld.d $fp, $sp, 40(0x28)
50: 02c0c063 addi.d $sp, $sp, 48(0x30)
54: 4c000020 jirl $zero, $ra, 0
0000000000000058 <main>:
58: 02ff8063 addi.d $sp, $sp, -32(0xfe0)
5c: 29c06061 st.d $ra, $sp, 24(0x18)
60: 29c04076 st.d $fp, $sp, 16(0x10)
64: 02c08076 addi.d $fp, $sp, 32(0x20)
68: 29bfa2c0 st.w $zero, $fp, -24(0xfe8)
6c: 1c000004 pcaddu12i $a0, 0
70: 02c00084 addi.d $a0, $a0, 0
74: 54000000 bl 0 # 74 <main+0x1c>
78: 1c000004 pcaddu12i $a0, 0
7c: 02c00084 addi.d $a0, $a0, 0
80: 54000000 bl 0 # 80 <main+0x28>
84: 29bfa2c0 st.w $zero, $fp, -24(0xfe8)
88: 50004000 b 64(0x40) # c8 <main+0x70>
8c: 24ffeacc ldptr.w $t0, $fp, -24(0xffe8)
90: 00150184 move $a0, $t0
94: 54000000 bl 0 # 94 <main+0x3c>
98: 2b7fb2c0 fst.s $fa0, $fp, -20(0xfec)
9c: 2b3fb2c0 fld.s $fa0, $fp, -20(0xfec)
a0: 01192400 fcvt.d.s $fa0, $fa0
a4: 24ffeacc ldptr.w $t0, $fp, -24(0xffe8)
a8: 0114b806 movfr2gr.d $a2, $fa0
ac: 00150185 move $a1, $t0
b0: 1c000004 pcaddu12i $a0, 0
b4: 02c00084 addi.d $a0, $a0, 0
b8: 54000000 bl 0 # b8 <main+0x60>
bc: 28bfa2cc ld.w $t0, $fp, -24(0xfe8)
c0: 0280298c addi.w $t0, $t0, 10(0xa)
c4: 29bfa2cc st.w $t0, $fp, -24(0xfe8)
c8: 28bfa2cc ld.w $t0, $fp, -24(0xfe8)
cc: 0040818d slli.w $t1, $t0, 0x0
d0: 0283940c addi.w $t0, $zero, 229(0xe5)
d4: 67ffb98d bge $t0, $t1, -72(0x3ffb8) # 8c <main+0x34>
d8: 0015000c move $t0, $zero
dc: 00150184 move $a0, $t0
e0: 28c06061 ld.d $ra, $sp, 24(0x18)
e4: 28c04076 ld.d $fp, $sp, 16(0x10)
e8: 02c08063 addi.d $sp, $sp, 32(0x20)
ec: 4c000020 jirl $zero, $ra, 0
函数的栈帧
从main()说起:
从可执行文件加载原理上看,系统会先start符号地址的代码,进行参数分析等流程,然后会跳转到main符号地址。在C语言里,main是个函数,编译器在编译这个函数时,会通过程序的栈,给main函数建立一个栈帧,也就是一小块存储空间,是个临时存储区,具体的存储数据可以看上面的图。
# 第一条指令是个立即数加法指令,作用是把当前的sp寄存器值减32,然后再存到sp里。
# sp 是栈指针寄存器,这个寄存器里,保存的是栈顶的地址,在 lp64d里,是64位的寄存器
# 需要注意的是,随着程序不断的调用函数,sp的值是从高向低“增长”的。
# 这条指令实际上是让栈顶向下“增长”了32个字节。
58: 02ff8063 addi.d $sp, $sp, -32(0xfe0) #1
# 第二条指令是把ra寄存器的内容放到 sp + 24 这个位置上,64位,这个指令使用基址+立即数的间接寻址指令
# ra寄存器的内容,是跳转到main符号地址准备好的,是main函数调用完成后的返回地址
# 这条指令的作用是保存好ra到栈上的存储空间
5c: 29c06061 st.d $ra, $sp, 24(0x18) #2
# 第三条指令是保存fp寄存器的内容到栈上存储空间,fp寄存器也就是r22,叫帧寄存器,里面存放的内容和fp类似
# 因为程序要对这个fp寄存器赋新值,所以要先保存下来,其实保存的是调用main函数前fp的值。
60: 29c04076 st.d $fp, $sp, 16(0x10) #3
# 第四条指令是个加法指令,作用是 fp = sp + 32
# 这条指令会改变fp寄存器的值,回看第一条指令 sp = sp -32
# 可以看出 tp - fp = 32 ,也就是说这4条指令的一个功能是,在tp和sp之间,设置了32个字节的临时存储空间
# 这个空间就叫栈帧,tp保存着最高地址,sp里保存着最低地址
# 如果main函数内又调用了一个函数,那么会在sp之下,再开辟出一个空间。
# 这样每个函数都有了一个栈帧。
64: 02c08076 addi.d $fp, $sp, 32(0x20) #4
后续就可以使用fp的值作为基地址,读取或保存到函数专属的栈帧里面了。
小结:main函数的专属栈帧有32个字节,从高到低,存放了8个字节的返回地址,8个字节的原fp地址,剩下的16字节用于参数传递和函数内临时变量存储。需要了解的是,栈帧的大小不是固定的,具体sp减多少,是编译器分析函数时,根据参数个数和临时空间需求计算决定的。比如main函数里调用的convert(),一上来就sp=sp-48,都是8的倍数。
函数调用参数的传递