【BSP开发经验】用户态栈回溯技术

前言

在内核中有一个非常好用的函数dump_stack, 该函数在我们调试内核的过程中可以打印出函数调用关系,该函数可以帮助我们进行内核调试,以及让我们了解内核的调用关系。同时当内核发生崩溃的时候就会自己将自己的调用栈输出到串口。 栈回溯非常有利于我们进行问题定位与代码跟踪。

在用户态如果想要展现出函数的调用栈,我们通常就需要使用gdb工具。在调试的时候可以使用gdb进行单步调试并显示栈。或者在程序崩溃的时候产生转储文件,再通过gdb进行分析崩溃时的程序堆栈。但是这样的工具似乎并不能完全替代dump_stack函数的作用。比如说通过dump_stack可以清晰的了解到一个函数是被从哪些地方进行的调用,以及通过dump_stack可以在一些错误的位置打印调用信息。

C库backtrace使用

#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#define BT_BUF_SIZE 100
void print_backtrace() {void *bt_buffer[BT_BUF_SIZE];int bt_size = backtrace(bt_buffer, BT_BUF_SIZE);char **bt_strings = backtrace_symbols(bt_buffer, bt_size);printf("backtrace:\n");for (int i = 0; i < bt_size; i++) {printf("%x %s\n", bt_strings[i]);}free(bt_strings);
}
int func_c() {print_backtrace();return 0;
}
int func_b() {return func_c();
}
int func_a() {return func_b();
}
int main() {return func_a();
}

上面是使用C库 backtrace进行栈回溯的例程,我们可以发现使用C库中的backtrace理论上可以轻松实现栈回溯功能。

在这里插入图片描述

但是嵌入式编译器往往对于这个接口的支持非常弱,很多情况下使用这个接口编译器是不支持的,就算支持很多时候是得不到函数的调用栈的,所以我们需要自己实现函数backtrace的功能。

ARM64 栈回溯实现

arm64的backtrace实现是最简单的,因为arm64 支持FP,且寄存器信息被存储于栈顶位置并且栈的结构非常固定。

arm64寄存器

下面是Arm64程序调用标准规定的通用寄存器的使用方法。

参数寄存器(X0-X7)函数参数数量小于等于8个时,使用X0-X7传递,大于8个时,多余的使用栈传递,函数返回时返回值保存在X0中。

调用者保存的临时寄存器(X9-X15) 调用者若使用到了X9-X15寄存器,在调用子函数之前,需要将X9-X15寄存器保存到自己的栈中,子函数使用这些寄存器的时候不需要保存和恢复。

被调用者保存的寄存器(X19-X29) 被调用者若使用到这些寄存器,需要将其保存到自己的栈中,返回时从栈中恢复。
特殊用途的寄存器

X8是间接结果寄存器。用于传递间接结果的地址位置,例如,函数返回一个大结构。

X16-X17过程内调用暂存寄存器。。

X18平台寄存器。

X29是栈帧(FP)寄存器。保存了调用函数的栈帧地址。

X30保存了返回地址(LR)。函数返回后跳转到该地址处运行。

arm64栈结构

在这里插入图片描述

arm64调用规则

实例代码:

nt func3()
{anycall_dump_stack();return 0;
}void func2()
{func3();
}void func1()
{func2();
}
int main()
{func1();
}

下图是main汇编代码

0000000000400804 <func3>:400804:	a9bf7bfd 	stp	x29, x30, [sp, #-16]!400808:	910003fd 	mov	x29, sp40080c:	97ffffc1 	bl	400710 <anycall_dump_stack@plt>400810:	52800000 	mov	w0, #0x0                   	// #0400814:	a8c17bfd 	ldp	x29, x30, [sp], #16400818:	d65f03c0 	ret000000000040081c <func2>:40081c:	a9bf7bfd 	stp	x29, x30, [sp, #-16]!400820:	910003fd 	mov	x29, sp400824:	97fffff8 	bl	400804 <func3>400828:	d503201f 	nop40082c:	a8c17bfd 	ldp	x29, x30, [sp], #16400830:	d65f03c0 	ret0000000000400834 <func1>:400834:	a9bf7bfd 	stp	x29, x30, [sp, #-16]!400838:	910003fd 	mov	x29, sp40083c:	97fffff8 	bl	40081c <func2>400840:	d503201f 	nop400844:	a8c17bfd 	ldp	x29, x30, [sp], #16400848:	d65f03c0 	ret000000000040084c <main>:40084c:	a9bf7bfd 	stp	x29, x30, [sp, #-16]!400850:	910003fd 	mov	x29, sp400854:	97fffff8 	bl	400834 <func1>400858:	52800000 	mov	w0, #0x0                   	// #040085c:	a8c17bfd 	ldp	x29, x30, [sp], #16400860:	d65f03c0 	ret

主要查看main函数的入口位置,函数的入口最早做的就是对函数跳转的现场进行保存:

40084c: a9bf7bfd stp x29, x30, [sp, #-16]!

这一行表示把上一个函数的FP和LR寄存器push保存到sp-16的位置上,并且对sp地址-16操作,也就是说对于 main 函数预留了16 bytes的堆栈空间进行使用。

400850: 910003fd mov x29, sp

第二行,表示更新main函数使用的堆栈帧地址到FP中。这样通过FP寄存器我们可以在后续调用中对main函数的栈帧再进行保存。参考后面调用func1函数的操作。

400854: 97fffff8 bl 400834 <func1>
这一步会执行跳转操作,同时会把返回地址更新到LR寄存器。

在FUNC1 子函数中,我们看到依然是同样的套路,第一步会先把FP和LR寄存器保存到堆栈中:

400834: a9bf7bfd stp x29, x30, [sp, #-16]!
这一行就把main函数使用的FP和LR寄存器保存到堆栈中了,并且对SP寄存器地址-16,含义就是预留了16 bytes的堆栈空间给func1使用。再接着看该函数的最后返回:

400844: a8c17bfd ldp x29, x30, [sp], #16
这里把上一级main函数使用的FP和LR从堆栈中恢复出来了。同时对sp寄存器执行+16操作,从而恢复上一级函数的堆栈指针现场,然后调用ret操作:

400848: d65f03c0 ret
这一行会自动把LR寄存器保存的地址赋值给PC,也就因此跳转回main函数继续运行。

arm64栈回溯方式

所以 arm64的栈回溯其实只需要不断对FP进行解引用,分别得到每一个栈帧的起始地址,然后就可以得到每一个栈中保存的函数返回地址与下一个栈帧地址。

代码大致如下:

在这里插入图片描述

实现效果

在这里插入图片描述

ARM 栈回溯实现

相对于ARM64 arm实现栈回溯要困难一些,因为arm的寄存器直接存储在栈底,需要借助FP去寻找到每一个栈底。

arm寄存器

arm栈结构

Arm 处理器总共有 37 个寄存器,其可以分为以下 2 类:

  1. 通用寄存器( 31 个)
    1. 不分组寄存器( R0 — R7 ),共 8 个。
    2. 分组寄存器( R8 — R14 )共22个(R8-R12,五个,一共52=10,R13-14,两个,一共是216=12,总共10+12=22个)
    3. PC 指针( R15 ),共1个
  2. 程序状态寄存器( 6个 )
    1. CPSR( 1个 )
    2. SPSR( 5个 )
      在这里插入图片描述

arm调用规则

想要比较容易的在arm中实现栈回溯需要在编译的是时候添加-mapcs -marm参数来保证 编译器编出按照固定规则入栈的代码。

000106e8 <func3>:106e8:	e1a0c00d 	mov	ip, sp106ec:	e92dd800 	push	{fp, ip, lr, pc}106f0:	e24cb004 	sub	fp, ip, #4106f4:	ebffffc0 	bl	105fc <anycall_dump_stack@plt>106f8:	e3a03000 	mov	r3, #0106fc:	e1a00003 	mov	r0, r310700:	e89da800 	ldm	sp, {fp, sp, pc}00010704 <func2>:10704:	e1a0c00d 	mov	ip, sp10708:	e92dd800 	push	{fp, ip, lr, pc}1070c:	e24cb004 	sub	fp, ip, #410710:	ebfffff4 	bl	106e8 <func3>10714:	e320f000 	nop	{0}10718:	e89da800 	ldm	sp, {fp, sp, pc}0001071c <func1>:1071c:	e1a0c00d 	mov	ip, sp10720:	e92dd800 	push	{fp, ip, lr, pc}10724:	e24cb004 	sub	fp, ip, #410728:	ebfffff5 	bl	10704 <func2>1072c:	e320f000 	nop	{0}10730:	e89da800 	ldm	sp, {fp, sp, pc}00010734 <main>:10734:	e1a0c00d 	mov	ip, sp10738:	e92dd800 	push	{fp, ip, lr, pc}1073c:	e24cb004 	sub	fp, ip, #410740:	ebfffff5 	bl	1071c <func1>10744:	e3a03000 	mov	r3, #010748:	e1a00003 	mov	r0, r31074c:	e89da800 	ldm	sp, {fp, sp, pc}

再添加-mapcs之后所有的入栈都将按照

   10734:	e1a0c00d 	mov	ip, sp10738:	e92dd800 	push	{fp, ip, lr, pc}

arm栈回溯实现

在这里插入图片描述

在这里插入图片描述

实现效果

在这里插入图片描述

MIPS 栈回溯实现

MIPS栈回溯相比于ARM与ARM64则更为复杂。因为MIPS平台,FP指针默认指向栈顶,而返回地址存在了栈底,所以说需要使用其他方法进行栈回溯。

MIPS寄存器

在这里插入图片描述

v0, v1: 用做函数调用的返回值。当这两个寄存器不够存放返回值时,就需要使用堆栈,调用者在堆栈里分配一个匿名的结构,设置一个指向该参数的指针,返回时v0指向这个对应的结构(由编译器自动完成)。

a0- a3: 用来传递前四个参数给子程序,不够的用堆栈。a0-a3和v0-v1以及ra一起来支持子程序/过程调用,分别用以传递参数,返回结果和存放返回地址。当需要使用更多的寄存器时,就需要使用堆栈,MIPS编译器总是为参数在堆栈中留有空间以防有参数需要存储。

fp: 不同的编译器对此寄存器的解释不同,GNU MIPS C编译器使用其作为帧指针,指向堆栈里的过程帧(一个子函数)的第一个字,子函数可以用其做一个偏移访问栈帧里的局部变量,sp也可以较为灵活的移动,因为在函数退出之前使用fp来恢复。

MIPS调用规则

在这里插入图片描述

如图 描述的是一种典型的(MIPS O32)嵌入式芯片的Stack Frame组织方式。在这张图中,计算机的栈空间采用的是向下增长的方式(MIPS架构没有专门入栈和出栈指令,栈的增长方向不定,可能是高地址向低地址增长,或是相反),SP(stack pointer)就是当前函数的栈指针,它指向的是栈底的位置。Current Frame所示即为当前函数(被调用者)的Frame,Caller’s Frame是当前函数的调用者的Frame 。
在没有BP(base pointer)寄存器的目标架构中,进入一个函数时需要将当前栈指针向下移动n字节,这个大小为n字节的存储空间就是此函数的Stack Frame的存储区域。此后栈指针便不再移动(在Linux内核代码TODO里面写着要加上在函数内部调整栈的考虑 – 虽然这通常不会发生),只能在函数返回时再将栈指针加上这个偏移量恢复栈现场。由于不能随便移动栈指针,所以寄存器压栈和出栈都必须指定偏移量,这与x86架构的计算机对栈的使用方式有着明显的不同。
RISC计算机一般借助于一个返回地址寄存器RA(return address)来实现函数的返回。几乎在每个函数调用中都会使用到这个寄存器,所以在很多情况下RA寄存器会被保存在堆栈上以避免被后面的函数调用修改,当函数需要返回时,从堆栈上取回RA然后跳转。移动SP和保存寄存器的动作一般处在函数的开头,叫做Function Prologue;
注意如果当前函数是叶子函数(不存在对其它函数的调用,就不保存ra寄存器,反之就保存)。恢复这些寄存器状态的动作一般放在函数的最后,叫做Function Epilogue。

我们可以看一下mips平台的反汇编代码:

004012fc <func3>:4012fc:	27bdffe0 	addiu	sp,sp,-32401300:	afbf001c 	sw	ra,28(sp)401304:	afbe0018 	sw	s8,24(sp)401308:	03a0f021 	move	s8,sp40130c:	0c100479 	jal	4011e4 <anycall_dump_stack>401310:	00000000 	nop401314:	0000c021 	move	t8,zero401318:	03001021 	move	v0,t840131c:	03c0e821 	move	sp,s8401320:	8fbf001c 	lw	ra,28(sp)401324:	8fbe0018 	lw	s8,24(sp)401328:	27bd0020 	addiu	sp,sp,3240132c:	03e00008 	jr	ra401330:	00000000 	nop00401334 <func2>:401334:	27bdffe0 	addiu	sp,sp,-32401338:	afbf001c 	sw	ra,28(sp)40133c:	afbe0018 	sw	s8,24(sp)401340:	03a0f021 	move	s8,sp401344:	0c1004bf 	jal	4012fc <func3>401348:	00000000 	nop40134c:	03c0e821 	move	sp,s8401350:	8fbf001c 	lw	ra,28(sp)401354:	8fbe0018 	lw	s8,24(sp)401358:	27bd0020 	addiu	sp,sp,3240135c:	03e00008 	jr	ra401360:	00000000 	nop00401364 <func1>:401364:	27bdffe0 	addiu	sp,sp,-32401368:	afbf001c 	sw	ra,28(sp)40136c:	afbe0018 	sw	s8,24(sp)401370:	03a0f021 	move	s8,sp401374:	0c1004cd 	jal	401334 <func2>401378:	00000000 	nop40137c:	03c0e821 	move	sp,s8401380:	8fbf001c 	lw	ra,28(sp)401384:	8fbe0018 	lw	s8,24(sp)401388:	27bd0020 	addiu	sp,sp,3240138c:	03e00008 	jr	ra401390:	00000000 	nop00401394 <main>:401394:	27bdffe0 	addiu	sp,sp,-32401398:	afbf001c 	sw	ra,28(sp)40139c:	afbe0018 	sw	s8,24(sp)4013a0:	03a0f021 	move	s8,sp4013a4:	0c1004d9 	jal	401364 <func1>4013a8:	00000000 	nop4013ac:	03001021 	move	v0,t84013b0:	03c0e821 	move	sp,s84013b4:	8fbf001c 	lw	ra,28(sp)4013b8:	8fbe0018 	lw	s8,24(sp)4013bc:	27bd0020 	addiu	sp,sp,324013c0:	03e00008 	jr	ra4013c4:	00000000 	nop

我们可以看出函数调用都是先使用addiu sp,sp xxx开辟栈,然后将使用 sw ra,xxx压栈保存在栈底。

MIPS栈回溯实现

在MIPS平台开始栈回溯的时候,我们可以获取的信息有寄存器SP,PC,RA的内容,使用PC和RA我们可以得到当前函数和上一级函数地址,问题在于怎样通过SP寻找到上一级函数的栈,在没有直接获取栈地址的方法的情况下需要通过进行代码分析来实现。

在这里插入图片描述

004012fc <func3>:4012fc:	27bdffe0 	addiu	sp,sp,-32401300:	afbf001c 	sw	ra,28(sp)401304:	afbe0018 	sw	s8,24(sp)401308:	03a0f021 	move	s8,sp40130c:	0c100479 	jal	4011e4 <anycall_dump_stack>401310:	00000000 	nop401314:	0000c021 	move	t8,zero401318:	03001021 	move	v0,t840131c:	03c0e821 	move	sp,s8401320:	8fbf001c 	lw	ra,28(sp)401324:	8fbe0018 	lw	s8,24(sp)401328:	27bd0020 	addiu	sp,sp,3240132c:	03e00008 	jr	ra401330:	00000000 	nop
  1. anycall_dump_stack 获取sp,ra寄存器地址,其中ra指向func3的0x401314。
  2. 从func3的返回地址(0x401314)开始向上进行命令查找,在0x401300的位置可以查找到ra寄存器入栈指令sw ra,xxx(0xafbf),取出立即数作为raoffset,其为返回地址在栈空间中的偏移。
  3. 继续向上在0x4012fc查找到开辟栈空间的指令addiu sp,sp,-32(0x27bd),去除立即数 stacksize 即为func3的栈空间大小。
  4. 如此ra=sp[raoffset/sizeof(long))] 就可以获取到func1的返回地址,即func2中的0x40134c。
  5. 然后nsp=sp+stacksize,可得func2的栈顶。
  6. 如此可继续向上回溯。

实现效果

在这里插入图片描述

对外接口

int anycall_backtrace(void **array, int size)

获取从当前函数开始的回溯结果保存于array,最大深度size。

char ** anycall_backtrace_symbols(void *const *array, int size)

解析array,并返回符号信息。

int anycall_dump_stack(void)

打印从当前位置开始的堆栈信息

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/329400.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

react 下拉框内容回显

需要实现效果如下 目前效果如下 思路 : 将下拉框选项的value和label一起存储到state中 , 初始化表单数据时 , 将faqType对应的label查找出来并设置到Form.Item中 , 最后修改useEffect 旧代码 //可以拿到faqType为0 但是却没有回显出下拉框的内容 我需要faqType为0 回显出下拉…

【实战教程】使用Spring AOP和自定义注解监控接口调用

一、背景 随着项目的长期运行和迭代&#xff0c;积累的功能日益繁多&#xff0c;但并非所有功能都能得到用户的频繁使用或实际上根本无人问津。 为了提高系统性能和代码质量&#xff0c;我们往往需要对那些不常用的功能进行下线处理。 那么&#xff0c;该下线哪些功能呢&…

QTextEdit 控件上显示信息:

目录 1. 使用 append 方法: 2. 使用 setPlainText 方法 3.例子&#xff1a; 1. 使用 append 方法: 如果你希望在 QTextEdit 控件上追加显示新的信息&#xff0c;可以使用 append 方法。例如&#xff0c;当你想要追加一行新的日志信息&#xff1a; self.text_edit.append(&…

卷积神经网络(CNN)详细介绍及其原理详解

卷积神经网络&#xff08;Convolutional Neural Networks&#xff0c;简称CNN&#xff09;是深度学习中非常重要的一类神经网络&#xff0c;主要用于图像识别、图像分类、物体检测等计算机视觉任务。本文将详细介绍卷积神经网络的基本概念、结构组成及其工作原理&#xff0c;并…

leetcode以及牛客网单链表相关的题、移除链表元素、链表的中间节点、合并两个有序链表、反转链表、链表分割、倒数第k个节点等的介绍

文章目录 前言一、移除链表元素二、链表的中间节点三、合并两个有序链表四、反转链表五、链表分割六、倒数第k个节点总结 前言 leetcode以及牛客网单链表相关的题、移除链表元素、链表的中间节点、合并两个有序链表、反转链表、链表分割、倒数第k个节点等的介绍 一、移除链表元…

集群分发脚本xsync

1.环境准备 1.准备三台服务器&#xff08;我这里使用虚拟机,操作系统 CentOS7 &#xff09;它们的IP分别为 192.168.188.135、192.168.188.136、192.168.188.137 2.先将三台机器的主机名修改&#xff0c;为每台主机设置hostname&#xff08;具体名称由自己定义&#xff09;&am…

https为何安全?

HTTPS&#xff08;超文本传输安全协议&#xff09;是一种用于安全通信的网络协议&#xff0c;它在HTTP协议的基础上通过SSL/TLS&#xff08;安全套接层/传输层安全&#xff09;协议来加密数据&#xff0c;以保护网络数据的传输安全。 TLS/SSL 基础概念 概念源自百度百科&…

电磁兼容(EMC):时钟电路PCB设计

目录 1. 布局 2. 布线 时钟电路做为产品内部的强辐射源&#xff0c;在设计阶段已经选用展频或者分频方案后&#xff0c;见另外接下来就需要对PCB的耦合路径进行规划设计。时钟电路具体的PCB设计具体要求如下&#xff1a; 1. 布局 结构干涉&#xff1a;时钟电路的晶振和法拉电…

BUUCTF---misc---我吃三明治

1、下载附件是一张图片 2、在winhex分析&#xff0c;看到一串整齐的编码有点可疑&#xff0c;保存下来&#xff0c;拿去解码&#xff0c;发现解不了&#xff0c;看来思路不对 3、再仔细往下看的时候也发现了一处这样的编码&#xff0c;但是这次编码后面多了一段base编码 4、拿去…

JS对象超细

目录 一、对象是什么 1.对象声明语法 2.对象有属性和方法组成 二、对象的使用 1.对象的使用 &#xff08;1&#xff09;查 &#xff08;2&#xff09;改 &#xff08;3&#xff09;增 &#xff08;4&#xff09;删&#xff08;了解&#xff09; &#xff08;5&#xf…

Linux文件:缓冲区、缓冲区刷新机制 | C库模拟实现

Linux文件&#xff1a;缓冲区、缓冲区刷新机制 | C库模拟实现 一、缓冲区的作用二、缓冲区的刷新机制三、测试样例解析3.1 测试样例和运行结果3.2 结果分析1、向显示器文件写入&#xff1a;2、向磁盘文件进行写入&#xff1a; 四、语言级别的缓冲区究竟在哪&#xff1f;五、C库…

网络原理3

运营商路由器&#xff0c;也可以把它当做一个NAT设备它就会对中间经过的数据包&#xff0c;进行网络地址转换当内网设备经过运营商路由器访问外网的时候就会把IP数据包中的源ip&#xff0c;替换成它自己的ip. 我的电脑要发送一个数据给cctalk服务器此时&#xff0c;我的电脑上就…

二叉树求解大小操作详解

目录 一、求所有结点个数 1.1 递归思路 1.2 递归分支图 1.3 递归栈帧图 1.4 C语言实现 二、求叶子结点个数 2.1 递归思路 2.2 递归分支图 2.3 递归栈帧图 2.4 C语言实现 三、求第K层的结点个数 3.1 递归思路 3.2 递归分支图 3.3 递归栈帧图 3.4 C语言实现 四、求…

高性能负载均衡的分类及架构分析

如何选择与部署适合的高性能负载均衡方案&#xff1f; 当单服务器性能无法满足需求&#xff0c;高性能集群便成为提升系统处理能力的关键。其核心在于通过增加服务器数量&#xff0c;强化整体计算能力。而集群设计的挑战在于任务分配&#xff0c;因为无论在哪台服务器上执行&am…

新火种AI|净利润上升628%,英伟达财报说明AI热潮还将持续

作者&#xff1a;一号 编辑&#xff1a;美美 AI大潮仍未放缓&#xff0c;英伟达再次超越预期。 今天凌晨&#xff0c;全球AI算力芯片龙头&#xff0c;被称为“AI时代卖铲人”的英伟达&#xff0c;正式公布了截至2024年4月28日的2025财年第一财季财报&#xff0c;其中第一财季…

java8总结

java8总结 java8新特性总结1. 行为参数化2. lambda表达式2.1 函数式接口2.2 函数描述符 3. Stream API3.1 付诸实践 java8新特性总结 行为参数化lambda表达式Stream Api 1. 行为参数化 定义&#xff1a;行为参数化&#xff0c;就是一个方法接受多个不同的行为作为参数&#x…

C++第三方库【JSON】— jsoncpp

目录 认识JSON jsoncpp库 安装&使用 认识jsoncpp Json::Value jsoncpp序列化 jsoncpp反序列化 认识JSON JSON(JavaScript Object Notation)是一种轻量级的数据交换格式&#xff0c;采用完全独立于编程语言的文本格式来存储和表示数据&#xff0c;常用于在客户端和服…

钉钉网页应用使用JSAPI报错,dd.alert提示errorCode:3.errorMessage:No value for message

问题分析&#xff1a; 起因是我用下图这个页面&#xff08;配置JSAPI鉴权&#xff09;的链接下载了JSAPI&#xff08;客户端API&#xff09;的SDK&#xff0c;但其实如图所示这个版本是2.10.3&#xff1a; 通过查看dingtalk-jsapi的npm版本&#xff0c;可以知道钉钉的JSAPI已…

c++设计模式-->访问者模式

#include <iostream> #include <string> #include <memory> using namespace std;class AbstractMember; // 前向声明// 行为基类 class AbstractAction { public:virtual void maleDoing(AbstractMember* member) 0;virtual void femaleDoing(AbstractMemb…

荣耀MagicBook X 14 Pro锐龙版 2023 集显(FRI-H76)笔记本电脑原装出厂Windows11系统工厂模式安装包下载,带F10智能还原

恢复开箱状态预装OEM系统&#xff0c;适用型号&#xff1a;HONOR荣耀FRI-H76、FRI-H56 链接&#xff1a;https://pan.baidu.com/s/1Lcg45byotu5kDDSBs3FStA?pwdl30r 提取码&#xff1a;l30r 华为荣耀原装WIN11系统工厂安装包&#xff0c;含F10一键恢复功能、系统自带所有驱…