RISC-V架构异常处理与栈回溯(一)

本文详细分析了RISC-V架构上的FreeRTOS任务栈帧结构,包括开启编译优化后fp指针的处理,以及如何实现异常处理时的栈回溯功能。通过示例代码和反汇编,展示了如何追踪函数调用关系和异常上下文信息。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

 RISC-V栈帧结构分析

开启编译优化后FP当成普通寄存器

实现自己的异常处理栈回溯函数

异常处理打印上下文和任务栈验证


        前面两篇文章分析了RISC-V上FreeRTOS的启动以及OS任务切换的处理流程,本文基于之前的分析,实现自己的栈回溯函数,程序出错陷入异常处理中,打印处函数调用关系及其上下文数据,方便以后问题调试。还是基于qemu模拟器,使用FreeRTOS官方标准发布包的FreeRTOS/Demo/RISC-V-Qemu-virt_GCC工程调试。

RISC-V栈帧结构分析

        写一个简单的test.c

long test_fun_b(long x, long y)
{
	printf("%s x:%ld y:%ld\r\n", __func__, x, y); 
	return x+y;
}

long test_fun_a(long m, long n, long x, long y)
{
	long b = 10;
	long c = 11;

	b = b + c + m;
	c = b + c + n;

	test_fun_b(b, c); 

	printf("%s b:%ld c:%ld\r\n", __func__, b, c);

	return b+c;
 
}

long test_fun_c(long x0, int x1, short x2, char x3, const char* x4, int x5, int x6, int x7, char x8)
{
	long b;
	long c;


	b = x0 + x1 + x2 + x3;
	c = x5 + x6 + x7 + x8;

	printf("%s b:%ld c:%ld %s x4:%d\r\n", __func__, b, c, x4, x8);

	b = test_fun_a(b, c, 0, 2); 

	return b+c;
}

int main(void)
{
        test_fun_c(1, 2, 3, 4, "test", 6, 7, 8, 9);

        return 0;
}

        使用RISC-V Linux编译器编译:riscv32-unknown-linux-gnu-gcc  test.c -o rv_test,

之后反汇编:riscv32-unknown-linux-gnu-objdump -S -d rv_test > rv_test.dsi

截取部分反汇编代码如下:

从进入函数test_fun_a开始分析,如下图所示,假设此时sp处在1位置,

执行完115行

   103f8: 7179                 addi sp,sp,-48

sp移动到了上图2位置,也就是在栈中留出了test_fun_a的栈帧,继续执完接下来两行代码,

   103fa: d606                 sw ra,44(sp)

   103fc: d422                 sw s0,40(sp)

则,ra和s0(s0也就是fp)都保存到当前函数栈帧中,如上图所示的位置。接下来执行  

   103fe:	1800                addi	s0,sp,48

s0的值更新了,指向了函数栈帧的开始位置,也就是上图sp位置1。继续执行,保存函数局部变量、参数a0,a1...等,直到141行调用函数test_fun_b时,test_fun_a的栈帧大致如下:

继续执行,进入函数test_fun_b,执行完test_fun_b前面3行代码,

   103be:	1101                	   addi	sp,sp,-32
   103c0:	ce06                		sw	ra,28(sp)
   103c2:	cc22                		sw	s0,24(sp)

此时s0的值还未修改,sw s0,24(sp)压栈保存的是test_fun_a栈帧的起始位置,栈帧如下图所示。

继续执行完

   103c4: 1000                 addi s0,sp,32

之后,s0又指向test_fun_b的栈帧起始位置,如下图所示:

如此循环,相当于一个链表把该任务的栈帧给串起来。发生错误时,我们直接可以找到任务函数栈帧的起始位置s0和任务栈的栈顶指针sp,也可以找到该函数的返回地址ra,通过ra的值也就找到了该函数的调用者,就可以分析出函数调用关系。

下面是一张RV64和ARM64栈帧对比图:

        以上分析用的是基于riscv32-unknown-linux-gnu-gcc编译器。FreeRTOS/Demo/RISC-V-Qemu-virt_GCC工程用的是riscv64-unknown-elf-gcc编译器,把上面的代码放入main_blinky.c中prvQueueSendTask中,

CFLAGS += -O0时,编译后反汇编,汇编代码如:

可以看到栈帧保存方式也是一样的,也就是符合上面的规则栈帧保存规则。

开启编译优化后FP当成普通寄存器

后来发现,CFLAGS += -O2 开启O2优化编译后反汇编,s0/fp被作为普通寄存器使用了,汇编代码如下:

可以看到s0被当作普通寄存器使用了,也就是没有了fp指针,但是ra还是符合上面的规则,放在函数栈帧的起始位置,而且反汇编中找不到test_fun_a和test_fun_b,实际上这两个函数被展开到test_fun_c里面了。这样任务栈如下图所示(s0作为普通寄存器,没有了fp指针):

         虽然riscv64-unknown-elf-gcc开启O2优化后没有fp指针,但是通过sp指针,结合反汇编代码还是可以找ra(反汇编代码函数入口第一行addi    sp,sp,-x,x即为函数栈帧的大小,sp还是指向栈顶),也就可以找到函数的Caller,可以手动完成栈回溯。

关于fp指针,可以参考文章[GCC入坑指南] -fomit-frame-pointer 和 -fno-omit-frame-pointer 编译标识 - 简书

实现自己的异常处理栈回溯函数

        基于以上的分析,我们就是可以实现异常处理栈回溯函数。在前一篇文章RISC-V FreeRTOS异常处理及任务切换分析(基于qemu+gdb跟踪调试)_Dingjun798077632的博客-优快云博客中分析了,同步异常处理的代码如下:

synchronous_exception -> handle_exception,如果不是ecall触发的异常则调用application_exception_handler -> freertos_risc_v_application_exception_handler。

函数freertos_risc_v_application_exception_handler是一个.weak 函数,最终进入死循环。

        所以,我们实现自己的freertos_risc_v_application_exception_handler函数,打印异常上下文信息与栈回溯。

        函数实现如下:参数mcause异常原因、mepc异常返回地址、mtval异常值寄存器(比如非法内存地址访问时,保存该内存地址值)、mstatus机器状态、pxTopOfStack异常发生时任务栈栈顶指针。

void freertos_risc_v_application_exception_handler(UBaseType_t mcause, UBaseType_t mepc, 
	UBaseType_t mtval, UBaseType_t mstatus, StackType_t * pxTopOfStack)
{
#if __riscv_xlen == 64
    #define portWORD_SIZE 8
    #define store_x sd
    #define load_x ld
#elif __riscv_xlen == 32
    #define store_x sw
    #define load_x lw
    #define portWORD_SIZE 4
#else
    #error Assembler did not define __riscv_xlen
#endif
    #define portCONTEXT_SIZE ( 31 * portWORD_SIZE )
    #define portCONTEXT_COUNT ( portCONTEXT_SIZE/portWORD_SIZE )

    int i;
    TaskStatus_t TaskStatus;
    UBaseType_t reg;
	StackType_t* pStack;
	UBaseType_t* fp;

    xprintf("\033[0m\033[1;31m%s\033[0m","Panic: Freertos risc-v application exception handler\n");
	//xprintf("Panic: Freertos risc-v application exception handler\n");
	xprintf("mcause:	0x%lx\nmepc:	0x%lx\nmtval:	0x%lx\nmstatus:0x%lx\npxTopOfStack:  %p\n", mcause, mepc - portWORD_SIZE, 
		mtval, mstatus, pxTopOfStack);

/* 
    参考portcontextRESTORE_CONTEXT中恢复context寄存器的顺序,打印异常发生前的各通用寄存器值
    load_x  x1, 1 * portWORD_SIZE( sp )
    load_x  x5, 2 * portWORD_SIZE( sp )
    load_x  x6, 3 * portWORD_SIZE( sp )
    load_x  x7, 4 * portWORD_SIZE( sp )
    load_x  x8, 5 * portWORD_SIZE( sp )
    load_x  x9, 6 * portWORD_SIZE( sp )
	... 
	...
*/
    xprintf("The exception context regs:\n");
    for (i = 1; i <= 28; i++){
        reg = *((UBaseType_t*)pxTopOfStack + i);
        if (i >= 2) {
            xprintf("x%d: 0x%lx\n", i+3, reg);
            if (i+3 == 8)
              fp = (UBaseType_t*)reg; //Risc-V寄存器x8即是s0/FP
        } else {
            xprintf("x%d: 0x%lx\n", i, reg);
        }
    }

    if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED ) 
	{
         vTaskGetInfo(NULL, &TaskStatus, pdTRUE, eInvalid);

        xprintf("Ther current task is %s, StackBase:%p, StackEnd:%p, StackTop:%p, StackHighWaterMark:%d\n", 
		    TaskStatus.pcTaskName, TaskStatus.pxStackBase, TaskStatus.pxEndOfStack, pxTopOfStack, TaskStatus.usStackHighWaterMark);

        if (TaskStatus.pxEndOfStack < pxTopOfStack + portCONTEXT_COUNT) {
            xprintf("Error: he current task stack error pxEndOfStack:%p pxTopOfStack:%p\n", TaskStatus.pxEndOfStack, pxTopOfStack);
            goto loop;
        }

        i = 0;
		//循环打印出函数的fp和ra返回地址, TaskStatus.pxEndOfStack为task栈底(最高地址)
        /*
            这部分通过fp查找ra开启O1或者O2优化后有问题,可以去掉(刚开始写文章时没发现开启优化fp被作为普指针)
        */
		xprintf("Ther current task bactrace:\n");
        while (fp < TaskStatus.pxEndOfStack) 
        {
            xprintf("fun%d: FramePointer:0x%lx\tReturnAddr:0x%lx\n", i++, fp, *(fp-1));
			fp = (UBaseType_t*)*(fp-2);
        } 

		//打印出当前栈数据,任务栈贞起始位置为pxTopOfStack + portCONTEXT_COUNT
		xprintf("Ther current task Stack:");
		i = 0;
		for (pStack = pxTopOfStack + portCONTEXT_COUNT; pStack < TaskStatus.pxEndOfStack; pStack++)
		{
			reg = *(UBaseType_t*)pStack;
			if (i++ % 8 == 0) {
				xprintf("\n%p:  0x%lx  ", pStack, reg);
			} else{
				xprintf("0x%lx  ", reg);
			}
		}

		xprintf("\n");
		//任务栈打印完成后,pStack必然指向pxEndOfStack
		if (pStack != TaskStatus.pxEndOfStack)
			xprintf("Error: the pStack not pointer to the EndOfStack\n");
    } else {

    }

loop:
	xprintf("Freertos risc-v application exception handler end\n");
	while(1);
}

        为了显眼,把freertos_risc_v_application_exception_handler的第一个打印Pannic用红色显示xprintf("\033[0m\033[1;31m%s\033[0m","Panic: Freertos risc-v application exception handler\n");

        上面代码中xprintf是自己写的printf函数,在中断处理中用xprintf代替printf。上面的宏定义portCONTEXT_SIZE、 portWORD_SIZE是从FreeRTOS\Source\portable\GCC\RISC-V\portContext.h中copy过来的(和架构相关)。

portContext.h中的portCONTEXT_SIZE宏定义如下:

        修改FreeRTOS\Source\portable\GCC\RISC-V\portASM.S的代码,在调用函数freertos_risc_v_application_exception_handler之前,通过a0-a4传入函数的参数(这里application_exception_handler之前a0已经保存为了mcause,a1保存的mepc + 4,之前的文章已经分析过了),portASM.S修改如下:

        同时freertos_risc_v_application_exception_handler中打印了当前任务的状态信息,需要开启部分FreeRTOS的部分配置:

在FreeRTOS\Source\include\task.h增加代码    

#elif ( configRECORD_STACK_HIGH_ADDRESS == 1 )

        StackType_t * pxEndOfStack;               /* Points to the end address of the task's stack area. */

修改后如下图(新增了57-58行):

打印任务栈时,需要通过vTaskGetInfo获取任务栈的栈底(栈起始位置)。

该函数代码FreeRTOS\FreeRTOS\Source\tasks.c

异常处理打印上下文和任务栈验证

        在之前测试函数test_fun_b中加入一条非法的指令,编译反汇编(这里需要在Makefile中将CFLAGS += -O2改为CFLAGS += -O0,关闭O2优化,否则test_fun_b和test_fun_a编译时会直接展开到test_fun_c,栈回溯没有了函数多重调用关系)。

        下面的测试还是基于CFLAGS += -O0编译的,一开始没发现CFLAGS += -O2会把fp当作普通寄存器使用,下面演示略过fp栈回溯部分,通过sp对照反汇编找到ra,手动完成栈回溯,开启O2依然起效,文章都写好了,就懒得改了。

        程序编译运行后,进入异常处理freertos_risc_v_application_exception_handler函数中打印如下(对照反汇编代码):

        对照反汇编代码,可以看到触发异常的pc地址为0x800008b8,该地址在函数test_fun_b中,而且刚好是增加的错误代码asm volatile(".word 0x1234567")。上面打印的最后一个函数func0的地址ReturnAddr为0x80000948,0x80000948对应的代码恰好是test_fun_a中调用test_fun_b函数的下一条指令的地址。

        对应的反汇编代码也可以看出,test_fun_b函数的栈帧大小为32byte, ra保存在sp偏移28的位置,从栈帧起始0x80082dd0 + 28byte的地方也是0x80000948。

        同样的方法也可以找到test_fun_a的ReturnAddr为0x80000a34,依此类推,最终找到任务的入口函数,如下图。

当我们修改test_fun_b函数,改为非法地址访问时

重新编译运行,可以看到mcause为0x00000007,mtval为0x00000034即为上面非法访问的地址

mcause 0x00000007对应Store sccess faut,之前mcause 0x00000002对应Illegal instruction。

        需要说明的是,一般Load和Store指令都是非精确异常,也就是mepc并不一定指向触发异常的指令地址,但mtval是准确的(指向访问出错的内存地址)。现代cpu为了提高效率,一般都支持超标量流水线,乱序执行,(比如当前乘法指令,下一条加法指令而且操作数不相干,乘法指令需要多个周期,加法指令就可能先执行完),加上Cach影响(比如当前指令Catch,下一条指令Catch命中)等...,cpu并不保证严格的遵守指令流水线。硬件外设驱动,有很多时候需要保证前一条指令先于后一条指令执行完,就需要用的Fence、Fence.i指令(对应arm也有dsb、dmb、isb指令),这里就不展开讨论了。

        下面时平头哥玄铁E907(RISC-V架构,arm等其它处理器也有类似的特性)用户手册对精确或者非精确异常的描述:

<think>好的,我现在需要帮助用户了解或实现得物签名的相关技术,特别是签名算法的实现方式。首先,我需要收集已有的信息。根据用户提供的引用,特别是引用[4]和[5],得物的签名算法可能涉及逆向工程分析,使用了JavaScript中的XMLHttpRequest重写来拦截请求,并在请求中包含特定的sign参数。这可能意味着签名生成是在客户端进行的,通常在前端JavaScript代码中完成。 接下来,我应该考虑常见的签名生成方法。常见的Web应用签名算法通常使用HMAC、AES、RSA或自定义的哈希组合。引用[1]提到了RSA算法,但得物的签名可能更倾向于使用HMAC-SHA256之类的哈希算法,结合时间戳、随机数和请求参数进行签名,以防止篡改和重放攻击。 用户可能需要具体的步骤,比如逆向分析得物的JavaScript代码,找到生成sign的函数,提取关键参数和加密逻辑。引用[5]中的代码片段展示了如何重写XMLHttpRequest的open方法来拦截请求,这可能用于调试和定位签名生成的代码位置。 此外,引用[4]提到使用Python进行算法还原,所以可能需要在Python中复现JavaScript的签名生成逻辑。这通常涉及分析请求参数如何排序、拼接,以及如何密钥结合进行哈希或加密。需要注意反爬机制,如obfuscation(代码混淆)或动态密钥,这可能需要使用AST(抽象语法树)解析或动态执行来还原算法。 用户可能还需要了解如何应对反调试措施,比如在Chrome开发者工具中使用断点调试,或者使用工具如Frida进行Hook。同时,引用[3]和[4]强调了法律和合规问题,确保研究仅用于学习目的,不用于非法用途。 最后,应该提醒用户注意法律风险,遵循网站的使用条款,避免侵犯知识产权或违反计算机安全法律。总结起来,实现得物签名需要逆向工程分析前端代码,提取关键算法,处理可能的混淆和反调试,最后用目标语言如Python复现算法。</think>### 得物签名算法实现方式分析 得物签名算法主要用于接口请求的身份验证和防篡改保护,常见实现方式涉及以下技术点: #### 1. **签名参数组成** 签名通常由以下参数动态生成: - 请求参数(经排序、过滤空值后拼接) - 时间戳(如:$timestamp=1620000000$) - 随机数(如:$nonce=5a8s3d$) - 设备指纹(如:$device\_id=abcdef$) - 应用密钥(加密盐值,可能动态获取)[^4] 示例参数拼接逻辑: $$ \text{sign\_str} = \text{path} + \text{sorted\_params} + \text{timestamp} + \text{nonce} $$ #### 2. **加密算法类型** 根据逆向分析,得物可能采用以下组合: - **HMAC-SHA256**:对拼接字符串进行哈希运算 - **AES/Base64编码**:对结果二次处理 - **自定义位移/异或操作**:增加逆向难度[^5] #### 3. **JavaScript代码混淆** 关键函数可能被混淆,例如: ```javascript function _0x12ab5(a, b) { return a ^ b << 3; } // 需要AST解析还原控制流 ``` #### 4. **Python算法还原示例** ```python import hmac import hashlib def generate_sign(params, secret_key): # 1. 参数排序并拼接 sorted_str = '&'.join([f"{k}={v}" for k,v in sorted(params.items())]) # 2. HMAC-SHA256加密 sign = hmac.new(secret_key.encode(), sorted_str.encode(), hashlib.sha256).hexdigest() # 3. 自定义处理(示例) return sign.upper() + str(int(time.time())) ``` #### 5. **反爬对抗措施** - 动态密钥:通过接口定期更新加密盐值 - 环境检测:验证是否在真机环境运行 - 请求频率限制:异常高频触发验证码[^5]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值