RISC-V 编程之 Frame Pointer和 backtrace

生成的代码

一段简单的代码:

int main(void)
{
    blink1(10);

    return 0;
}

未设置任何优化选项得到的编译结果,函数头部的sw s0,8(sp)addi s0,sp,16和函数尾部的lw s0,8(sp)就是对 frame pointer 的存储和恢复。s0 寄存器即 fp 寄存器:

00000118 <main>:
 118:	1141                	addi	sp,sp,-16
 11a:	c606                	sw	ra,12(sp)
 11c:	c422                	sw	s0,8(sp) 
 11e:	0800                	addi	s0,sp,16
 120:	4529                	li	a0,10
 122:	00000097          	auipc	ra,0x0
 126:	000080e7          	jalr	ra # 122 <main+0xa>
 12a:	4781                	li	a5,0
 12c:	853e                	mv	a0,a5
 12e:	40b2                	lw	ra,12(sp)
 130:	4422                	lw	s0,8(sp)
 132:	0141                	addi	sp,sp,16
 134:	8082                	ret

使用 -fomit-frame-pointer 优化选项得到的编译结果,没有对 s0/fp 寄存器的操作代码:

000000d6 <main>:
  d6:	1141                	addi	sp,sp,-16
  d8:	c606                	sw	ra,12(sp)
  da:	4529                	li	a0,10
  dc:	00000097          	auipc	ra,0x0
  e0:	000080e7          	jalr	ra # dc <main+0x6>
  e4:	4781                	li	a5,0
  e6:	853e                	mv	a0,a5
  e8:	40b2                	lw	ra,12(sp)
  ea:	0141                	addi	sp,sp,16
  ec:	8082                	ret

使用 -O2 优化选项得到的编译结果,也没有对 s0/fp 寄存器的操作代码,因为 -O2 选项包含了 -fomit-frame-pointer 选项:

00000000 <main>:
   0:	1141                	addi	sp,sp,-16
   2:	4529                	li	a0,10
   4:	c606                	sw	ra,12(sp)
   6:	00000097          	auipc	ra,0x0
   a:	000080e7          	jalr	ra # 6 <main+0x6>
   e:	40b2                	lw	ra,12(sp)
  10:	4501                	li	a0,0
  12:	0141                	addi	sp,sp,16
  14:	8082                	ret

使用 -O2 -fno-omit-frame-pointer 优化选项得到的编译结果,函数头部有sw s0,8(sp)addi s0,sp,16,函数尾部有lw s0,8(sp),但是次序和前面的不一样了,这是编译优化导致的编译乱序:

00000000 <main>:
   0:	1141                	addi	sp,sp,-16
   2:	c422                	sw	s0,8(sp)
   4:	c606                	sw	ra,12(sp)
   6:	0800                	addi	s0,sp,16
   8:	4529                	li	a0,10
   a:	00000097          	auipc	ra,0x0
   e:	000080e7          	jalr	ra # a <main+0xa>
  12:	40b2                	lw	ra,12(sp)
  14:	4422                	lw	s0,8(sp)
  16:	4501                	li	a0,0
  18:	0141                	addi	sp,sp,16
  1a:	8082                	ret

对调试的影响

有没有 frame pointer 对调试时函数调用栈的查看没有影响,至少对 RISC-V 而言是这样的。调试器的调用栈是 CFI – Call Frame Information 提供的,不是通过 frame pointer 获取的。CFI 会告诉调试器返回地址存储在堆栈的哪个位置,调试器依次去获取每级函数调用的返回地址即可获得调用关系,除了调用关系外,调试器还得知道局部变量的存储位置等信息,这些信息都由 CFI 来实现。

当给编译器添加 -g 选项后,编译器的汇编输出会多出很多 .cfi_* 的伪指令,这就是 CFI 有关的内容了,还有就是 .loc 的伪指令,这是将指令和源代码行号对应起来的伪指令。例如某文件加 -g 编译的汇编输出:

	.file	"frame-pointer.c"
	.option nopic
	.text
.Ltext0:
	.cfi_sections	.debug_frame
	.align	1
	.globl	blink4
	.type	blink4, @function
blink4:
.LFB0:
	.file 1 "frame-pointer.c"
	.loc 1 14 1
	.cfi_startproc
.LVL0:
	.loc 1 15 5
	.loc 1 14 1 is_stmt 0
	addi	sp,sp,-16
	.cfi_def_cfa_offset 16
	sw	s0,8(sp)
	sw	ra,12(sp)
	.cfi_offset 8, -8
	.cfi_offset 1, -4
	.loc 1 14 1
	mv	s0,a0
.LVL1:
	.loc 1 16 5 is_stmt 1
	call	backtrace
.LVL2:
.L2:
	.loc 1 17 5 discriminator 1
	.loc 1 18 9 discriminator 1
	call	hal_led_blue_toggle
.LVL3:
	.loc 1 19 9 discriminator 1
	.loc 1 20 10 is_stmt 0 discriminator 1
	addi	s0,s0,-1
.LVL4:
	.loc 1 19 9 discriminator 1
	li	a0,1
	call	hal_tick_delay_lf
.LVL5:
	.loc 1 20 9 is_stmt 1 discriminator 1
	.loc 1 21 5 is_stmt 0 discriminator 1
	bnez	s0,.L2
	.loc 1 22 1
	lw	ra,12(sp)
	.cfi_restore 1
	lw	s0,8(sp)
	.cfi_restore 8
.LVL6:
	addi	sp,sp,16
	.cfi_def_cfa_offset 0
	jr	ra
	.cfi_endproc
.LFE0:
	.size	blink4, .-blink4

frame pointer 的作用

frame pointer 被程序用来跟踪函数调用关系(backtrace功能),特别是在发生异常时,输出函数调用关系可以更容易跟踪问题所在,特别是异常发生现场是被多处调用的公共函数时。

为了产生有 frame pointer 的栈帧,如果有优化选项,那么要加上 -fno-omit-frame-pointer 告诉编译器不要把 frame pointer 优化掉,还要加上 -fno-optimize-sibling-calls 选项,该选项让编译器不要优化尾部调用,如果执行了尾部调用,那么本来应该编译成 jal, ret 两条指令的操作被优化成 j 一条指令,而且 frame pointer 在 j 指令前被恢复了,这就没法获得完整的调用关系了。

C代码,blink2 函数的末尾调用 blink3 函数:

void blink2(uint32_t n)
{
   ……
    blink3(n);
}

加了 -fno-optimize-sibling-calls 选项的编译结果,尾调用未被优化:

20400070 <blink2>:
20400070:	1141                	addi	sp,sp,-16
20400072:	c422                	sw	s0,8(sp)
20400074:	c04a                	sw	s2,0(sp)
20400076:	c606                	sw	ra,12(sp)
20400078:	c226                	sw	s1,4(sp)
2040007a:	0800                	addi	s0,sp,16
……
2040008e:	854a                	mv	a0,s2
20400090:	3775                	jal	2040003c <blink3>
20400092:	40b2                	lw	ra,12(sp)
20400094:	4422                	lw	s0,8(sp)
20400096:	4492                	lw	s1,4(sp)
20400098:	4902                	lw	s2,0(sp)
2040009a:	0141                	addi	sp,sp,16
2040009c:	8082                	ret

没有加 -fno-optimize-sibling-calls 选项编译结果,尾调用被优化:

20400070 <blink2>:
20400070:	1141                	addi	sp,sp,-16
20400072:	c422                	sw	s0,8(sp)
20400074:	c04a                	sw	s2,0(sp)
20400076:	c606                	sw	ra,12(sp)
20400078:	c226                	sw	s1,4(sp)
2040007a:	0800                	addi	s0,sp,16
……
2040008e:	4422                	lw	s0,8(sp)
20400090:	40b2                	lw	ra,12(sp)
20400092:	4492                	lw	s1,4(sp)
20400094:	854a                	mv	a0,s2
20400096:	4902                	lw	s2,0(sp)
20400098:	0141                	addi	sp,sp,16
2040009a:	b74d                	j	2040003c <blink3>

上述两次编译都添加了选项 -O2 -fno-omit-frame-pointer。从上述两次编译结果看,尾调用优化只减少了一条指令,但是可以减少堆栈空间的使用,因为执行尾调用时,caller 的堆栈空间已经释放了。

-fno-optimize-sibling-calls 选项类似可能还有 -ftree-tail-merge 选项等。

backtrace 的实现

gcc 有提供内建函数 __builtin_frame_address 获取栈帧地址,还提供内建函数 __builtin_return_address 获取函数返回地址。但是 gcc 的这两个函数只保证正确获取末级的帧地址和返回地址,上级的帧地址和返回地址是不保证正确的,实际使用情况是,RISC-V gcc 的这两个函数无法得到上级的帧地址和返回地址,那么就只能自己写汇编代码来实现 backtrace 了。

示例代码如下:

    .text
    .align  2
    .global backtrace
    .type   backtrace, @function
backtrace:
    la      a0, backtrace_buffer
    sw      ra, 0(a0) // 保存末级函数返回地址
    mv      a1, s0 //  取fp寄存器,s0即fp

    lw      a2, -4(a1)  // fp寄存器所指向的位置偏移-4就是上一级返回地址的存储地址
    sw      a2, 4(a0)
    lw      a1, -8(a1) // fp 寄存器所指向的位置偏移-8就是上一级fp的存储地址

    lw      a2, -4(a1) // 依此类推
    sw      a2, 8(a0)
    lw      a1, -8(a1)

    lw      a2, -4(a1)
    sw      a2, 12(a0)
    lw      a1, -8(a1)

    lw      a2, -4(a1)
    sw      a2, 16(a0)
    lw      a1, -8(a1)

    lw      a2, -4(a1)
    sw      a2, 20(a0)
    lw      a1, -8(a1)

    ret
    .size   backtrace, .-backtrace

    .data
    .align  2
backtrace_buffer:
    .word   0, 0, 0, 0, 0, 0, 0, 0

这段代码将各级函数调用的返回地址写入到 backtrace_buffer 中,这里只是个示例,并不实用,除了末级函数外,上级函数调用的返回地址获得可以通过循环来实现,这里的代码是全部展开的,而且也没有判断是不是顶级函数。

参考

-gcc优化选项解析
-Tackling C++ Tail Calls

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值