首页
社区
课程
招聘
[原创]阿里 sgmian 第三篇:签名命令分析 70102 之 VMP 分析
发表于: 1小时前 37

[原创]阿里 sgmian 第三篇:签名命令分析 70102 之 VMP 分析

1小时前
37

时间: 2026 年 11 月 08 日 星期日 01:37:35 CST
作者: 人生导师

哈哈哈哈哈哈哈哈哈哈哈哈,成了,没想到我的方法在阿里身上更加好用,哈哈哈哈哈
其实也可以说是对阿里四神虚拟机的分析,希望能拿精华吧,简历上就缺一行然后格式就好看了

成了归成了,过程还是得记下来,不然下次换台 VM 又要从头摸。

先把结论摆这儿:淘宝这台 VM 不是我一开始以为的「另一个抖音」。

它是 direct-threaded 解释器,状态几乎全在寄存器里,handler 表只有一张;真正难的不是找 handler,是让 IR 在变长字节码槽、嵌套帧和 native 调用之间不散架。

按之前的叫法,淘宝这台我叫它四神。它和京东、快手、得物一样,属于「handler 内部直接算」的那一类,不是六神那种 OLLVM Pass 级 VMP。

虚拟机本身只有一页纸的入口:

命令层的参数封送、返回值组装在 2.70102 那篇已经铺过,这篇只讲 VM 本身:它怎么跑,怎么从 trace 还原到 IR。

我一开始把阿里想得太复杂了:一看到 vm_invoke 这种名字,下意识以为它是虚拟机入口,结果它只是个命令调度器——一张表、查表、调用。真正执行字节码的是它下游的 0x170D1C,一个十几条指令就能讲完的 direct-threaded 解释器。

这台 VM 和抖音七神最大的区别有两条:

代价是字节码槽是变长的:0x10 / 0x20 / 0x30 都有,还有调用/跳转后直接 [x21] 取指的形态。你要是假设「一条 VM 指令占一个 0x10 槽」,IR 的 vIP 会全错。

Java 侧 doCommandNative(70102, Object[]) 进来之后,先到 jni_doCommandNative(0x5A2FC),然后:

这里最容易误判的是 vm_invoke。它是重入的:

命令号第一段区分世界:a=7 是业务命令,a=1 是库命令。所以看到 vm_invoke 不能直接把它当虚拟机,它只是 L1 调度层;真正的 VM 是 g_vm_hook_fn 下游那个拿 x21/x23/x25/x27 的解释器。

0x170D1C 的参数约定是这样的:

入口先把它们钉死:

x3 是入口槽号,x21 = x22 + x3*0x10。这条式子很重要:它说明字节码槽的步长是 0x10,而且调用 VM 函数时传的是「函数入口相对字节码基址的槽号」,不是绝对地址。

ctx 的布局(按 x27 量):

这里有个细节要单独说:ctx+0x8 不是 vPC 本体,只是解释器每轮把 x21 存一份的簿记槽。真正决定下一条指令的是 x21。七神那台反过来,vPC 是 ctx+0xC 的内存槽(x23 指向它),字节码源在 x20,当前指令在 x8。两台的锚点不能混。

这台 VM 的锚点全是寄存器,入口一次定死,后面不再变:

锚点一律按 offset 认,不按名字认。 同一个 offset 上 w23/x23 是两个视图(size 4 和 8),按名字认会漏。这个坑在七神那篇已经踩过一次,这边直接按 offset 写进 lifter.py 的 ANCHOR:

和抖音那台的对照:

这张表就是移植的全部工作量。dy_7/VMP 那套 parse → matcher → lifter2 的结构没动,换的是 ANCHOR / SPACE / CTX_OFF / FILES / IP 这五个常量。

每个 handler 的最后四条基本是固定的:

切段要认的是 str x21,[x27,#8] 紧跟 br,不是「br 的寄存器来自 x23 表」。 原因是有条件跳转 handler 两个分支各自算自己的下一条:

两个分支都能查到自己的表项,所以「br 的寄存器来自 [x23, …]」这条判据在命中的分支上会漏。按簿记指令认,两个分支都会落在同一个段边界上。

拿一个最干净的 OR handler 看:

对应的 IR 一行:

0x221 是 opcode,0x179d00 是 handler,14834 是本次执行次数,<0x2> 是回写后的 vIP。opcode → handler → 语义这张表就是这么长出来的。

一条 VM 指令的槽布局:

N 不是常数。分发尾巴里的预增量就是答案:

这里要纠正 2.5 那篇的一个统计口径:那里还列了 399 条 ldrh w8,[x21,#0xa]。我后来查了,这 399 条全部来自 0x1afd20,那是个普通 native 函数,x21 在里面是另一个结构体指针,不是 VM 的 vPC。把它们算进 dispatch 会让总数虚高 399。真正的 dispatch 是:

和切段数 690,815 只差 2 条(收尾和跨深度的边界段),口径基本对得上。这个口径我当时没细看,后来对总数才发现,算是给「数字必须自洽」交了一次学费。

四条帧协议指令要单独记:

prologue 的实际动作(x25 是 vreg 基址):

return 反过来:

vreg 的角色划分也就清楚了:

8845 对 prologue/return、3781 次 call、161 次 exit、最大深度 3。这台 VM 有自己的调用约定,而且调用约定本身就是字节码语义的一部分。 所以 IR 里 @PROLOGUE、@RET、@CALLVM 不是装饰,是控制流骨架。

matcher.py 是流式的,规则只有一条:

一次 dispatch 跳转(br)收一段;段 = 上一次 dispatch 到这一次 dispatch 之间的完整执行切片,就是一个 handler。

ENTRY(0x170D1C) / EXIT(0x170AC4) 不是段边界,只是深度标记:执行到 ENTRY 深度 +1,执行到 EXIT 深度 -1。段可以跨 ENTRY/EXIT——复合 handler 里 blr 进嵌套 VM 时,外层段不会断,嵌套执行完继续回到外层段。interpreter.py 在段尾检查深度,如果 EXIT 把深度打回段首以下,就在 EXIT 处截断,避免把外层帧的原生代码混进来。

实测:

214 个 opcode 里,纯直线 133、块内条件分支 81,这两类正好凑满 214;另外有 13 个 handler 带 bl/blr,其中 4 个是复合指令:

这里的「分支」是块内条件,不是跨段的跳转。跨段跳转由 ip 线表达,下一节说。

整条流水线是:

这份 trace 是 DuckDroid 用 --stage 70102 --trace DIR 产出的:init 在 trace 开始前跑完,arm 在 doCommandNative 入口(libsgmainso+0x5A2FC),到它的 ret 结束;后台线程和 init 命令都不记录。两个实现细节决定了这份 trace 能不能用:

复跑对拍用 TAOBAO_RANDOM_SEED + TAOBAO_WALL_TIME_NS 固定熵流/时钟,否则每次熵流和时钟字段都会变,trace 没法逐条对。

淘宝这台不用 keystone 回汇编,原因很实际:

pypcode 用的是 AARCH64:LE:64:v8A。每译一条指令先插一个 IMARK,A64 定长 4 字节,所以 IMARK 和指令一一对应,用它当边界切 shadow:

值一律来自 trace 的 read/write 列,不来自求值:

x/w 是同一个物理槽的两个视图,必须按 offset 合并,再按 size 分层保留:

不这么干,x10 会被 w10 吃掉,后面所有值全错。

taint() 只做一件事:单趟扫 shadow,给每个 varnode 记「你的值派生自哪条锚点线」。

DEREF 表就是「指针 → 指针指向的值」:

x21 线、x23 线、x25 线、x24 线都是同一个函数换个种子。先用种子把锚点线拉出来,再谈语义,这是整套方法的核心顺序。

操作数线不是从 handler 头往下推,而是从写虚拟寄存器文件的 STORE 起,往上游跟 def-use。

理由和七神那篇一样:一条 VM 指令的结果就落在那几个 STORE 上。往前推是发散的,往回追是收敛的——每个 varnode 只有一处定义。p-code 不是严格 SSA,但块内是顺序语义:「use 之前最后一次写」就是它的 def,反向走是确定的,不用解数据流方程组。

根只有四类:

helper 里的槽位是 spill/reload + 别名算出来的,静态锚点认不准。所以 _ctx_delta() 沿 def 链往回走,根必须落在:

路上只允许 COPY、零符号扩展、常量加减、栈上 spill 再 reload。走到别的寄存器/算式就认不出,认不出就按裸指针 * 输出,不猜。这条「宁缺勿错」在 helper 里特别重要,因为 helper 有大量自己的栈帧和自校验状态机,认错了整段 VREG 都会废。

_img_addr() 负责另一件事:这个访存地址到底在 VM 镜像里,还是在运行期对象上。

SLEIGH 把 BFI/UBFX/LSR 展成 shift/or/and 链,一条指令能膨胀成几十个算符。这些是解码头,不是 VM 语义。不折的话,IR 就是一堵墙。

这台用的办法是追字节道:算式的第 k 个字节是从哪个内存字节来的。位对齐(整字节的移位/掩码)才追得动,追不动就不折。

够不够格折成「读」,三条判据缺一不可:

两个坑:

csel 也要特殊处理。A64 的 csel 没有 CSELECT op,SLEIGH 展成「CBRANCH + 同一格 unique 在分支前后各定义一次」。反向线性取「use 前最后一次写」只拿到落空那一支,条件整支丢。所以 _branches() 把 CBRANCH 夹出来的条件抓回来,_fold() 再把条件折成常量的 csel 收成一支。条件跳转的语义全在条件上,丢了条件等于丢了这条 handler。

一条 VM 指令一个 vline(可以有多行 IR),列和 code.log 对齐:

举例:

几条渲染规则:

整套东西拆到最后,其实就是三条线:

x23 只做两件事:取值、被 br。ldr xN,[x23,xN,lsl#3] 的 xN 是 opcode,查出来的就是 handler。切段按上一节的尾巴签名,不按 handler 内部。

ip_line() 取三个东西:

正常 dispatch 的 tree 是 ip + 0x10;条件/跳转/返回 handler 是 csel / ip + N*0x10 / VREG[0x1f]。str x21,[x27,#8] 是簿记,不在这条线上,_is_ip() 里专门把它排除掉。

_slot_index() 把绝对地址归一成槽号:

这样 vIP 跨 run、跨线程稳定,不会因为 ctx 地址变了就全乱。

operation() 扫所有 STORE:

种子是所有 STORE,不是「写 VREG 的 STORE」。这一点在七神那篇被打过脸:VM 的 store 指令一个字节都不落 VREG 文件,种子太窄会整类被过滤掉,看起来像「这条 handler 没算东西」。这边从一开始就放宽了。

全量 19,179,756 条 trace 跑一遍:

669,183 和 690,815 差的那 21,632 段,就是「只有 ip+0x10 隐含推进、没有数据写、没有显式分支」的段。它们不是丢了,是本来就没有可渲染的语义。

随便截一段真实 IR(out/handlers):

这屏 IR 里规律会自己浮出来:

把 pre -> post 串起来,就是一台 VM 的执行序列:ip+0x10 是顺序、跳变是分支、回跳是循环。CFG 不用单独造,从这列就能长出来。

13 个带 bl/blr 的 handler 里,有 4 个是真正的复合指令,内部会再进 VM:

这里的「次数」是 handler 执行次数,不是嵌套 VM 次数。真正进嵌套 VM 看 @VM_ENTER(161 次),别把 4,836 当成 4,836 次重入。

0x18C870 是最干净的样本:

call_vm 的返回值是被调 VM 函数的字节码入口;返回 0 表示长返回(被调函数已经把返回地址塞回 vreg[15])。所以 x21 = x0 是「跳进 VM 函数」,不是「函数返回」。0x18F2DC 多做了 orr/and,0x16FA70 用 call_vm2 走运行期方法对象,0x173E3C 走另一条 helper。

这四个之外,还有 resolve_native / lookup 两条不调用函数的「机制调用」:

vm_invoke 是另一条固定流程,不用看字节码:

本 trace 373 次 = 340 handler + 33 hook,两个出口严格一一对应。hook 落点 0x1B4DC0,就是 VM 的重入入口。

native 调用的 ABI 也固定:

native_checkpoints.tsv 有 5,857 个检查点,每个都记录 x0..x6 的符号表达式、callee 真正消费的实参上的 Java 依赖、额外实参。当前 sym_gaps=0。native 屏障的意思是:只锁 ABI,不进去;每次 native 调用后 mem 缓存作废,避免把 callee 内部的写当成 VM 的写。

前面一直在讲 VM 怎么跑,但 VM 并不认识 Java 对象。它看到的只是一块 216 字节的 sgm_cmd_args。从 Object[] args 到 VREG[0x10],中间有三层手:

这一节按这三层拆。

本 trace 的 Java 入参(hook_a.log):

Java 侧签名是 doCommandNative(int id, Object[] args),所有元素都是 boxed object。native 侧不能直接当 int/bool 用,必须走 JNI 取。

jni_doCommandNative(0x5A2FC)的头几行:

这里要纠正一个说法:b 不是 hash,是 (id % 10000) / 100。反编译里的 (5243 * x) >> 19 是「除以 100」的倒数乘法,5243 / 2^19 ≈ 1/100。所以 70102 拆成 (7,1,2),70703 拆成 (7,7,3),不是随机 hash。

ctx_jni 是栈上的 0x20 字节:

flag=1 表示走 native 表;status 是另一个 int,失败时 throw_sec_exception()。

vm_invoke(a,b,c,flag,ctx,status)(0x56F70):

(7,1,2,1) 在 native 表里查到 mod+0xD002C,也就是 cmd70102_handler。注意这一步还不是 VM:它只是命令 handler,和 (1,32,12) 那些库命令共用同一张调度器。

cmd70102_handler 在栈上开了一个 sgm_cmd_args args(216 字节),先把 args.str0.len 起的 0xD0 字节清零,再给几个短字符串预置栈缓冲:

str7 / str8 不预置(s=NULL, cap=0),只有非 null 且长度达到或超过 cap 时 getter 才 malloc。

然后按 Java 下标取:

字符串 getter 的动作(jni_get_string_arg 0x154744):

jni_get_bool_arg 是 GetObjectArrayElement → Boolean.booleanValue → &1;jni_get_int_arg 是 GetObjectArrayElement → Integer.intValue。都是「先取元素,再 unbox」,不是直接读 jintArray。

sgm_cstr 和 sgm_cmd_args 的布局:

本 trace 的实际值:

这里有两个细节:

vmrun(args, out, status)(0xD3410)先做两个检查:

然后把 args 和 8 个输出槽打包:

flag=0 走 script 表;表里没有 (7,1,2),于是落到 g_vm_hook_fn(0x1B4DC0)→ 0x1B0C50 → VM 解释器。

注意这里换了一个 ctx。 JNI 入口的 ctx_jni 是 {a,b,c,0,env,Object[]};vmrun 传给 VM 的 ctx 是 {&sgm_cmd_args, &out_slots}。VM 的 ctx[0] 不是 JNIEnv,是参数结构;ctx[1] 是输出槽。两套 ctx 不要混。

VM 入口 0x170D1C 拿到的 x1 是 v64。它把 x1 放进 x27(ctx),之后某条 VM 指令执行:

从这以后,VM 里的 Java 参数就是 VREG[0x10] 指向的那块 216 字节。字段访问全是固定偏移:

这张表就是「Java 参数在 VM 里到底从哪进」的完整清单。几个观察:

vmrun 把输出槽 strdup 成 out[0..7],cmd70102_handler 建一个 java.util.HashMap,按字段名 put 进去,header_map_to_java 用 NewObject + NewStringUTF + CallObjectMethod 返回 Java 对象。输入是 Object[] → sgm_cmd_args,输出是 out[] → HashMap;两条路在 cmd70102_handler 里交汇,但方向相反。

tools/sym_check.py 不是还原器,它是值流闭包检查:把 VM IR 当成单路径程序,符号化执行一遍,看每个值有没有来源、每个 native 实参能不能对上 VM 槽。

模型:

核心就两个函数,一读一写(简化版),正好对应「完整」和「对称」:

read_slot 发现槽里没有值,就记一条 gap;write_slot 发现旧 def 一次都没被读过就被覆盖,就记一条 orphan。这两个计数器就是 sym_gaps / sym_orphans 的来源。

它输出四样:

「完整」看 sym_gaps=0:没有哪个操作数是从空气里来的。每个读都能追到常量、Java 输入、ctx/module 基址、或某个 def;追不到就是 gap。

「对称」看两头:

provenance 精确字段核对 14 对 / 0 不对,是这套检查的抽样对账:挑了 14 个直接 load 的字段,符号来源和实测值逐一对,全部对上。

要说清楚它的边界:这是单路径检查,按 trace 顺序跑,不枚举分支;native 只锁 ABI,不进内部;heap 载荷和外部返回值记成 ?unknown(origin),不算缺值。它能回答「这次执行的值流有没有断、VM 槽和 native 实参对不对得上」,不能回答「任意输入下语义是否等价」。但对我们这种「先保证这次 trace 的骨架是完整的」的目标,够了。

下面是 out/handlers 的头部原文(; 开头是注释)。参数映射、VM 内使用点、native ABI、符号检查都在这。三处小改:b 那一行按反编译改成 (id%10000)/100(原文件写的是 hash);第一行那句指向还原用途的话删了,免得又把这篇写成还原入口;appKey 和 str1 的 token 前缀打码成 <appKey> / at88…。其余照原文。

文件头里 flag = 8 (Java args[11]) 是简写:Java 给的是 0,8 是 vmrun 的配置开关置的,见 11.4。再往后就是逐行 IR(vline / vIP / off / seq / kind / ir),第 9 节已经截过一段。

这台 VM 的「骨架」已经能看了,但离「还原成伪 C」还差几步:

顺序还是那句:先出块,再出 CFG,伪 C 排最后。 反了做不出来。

从最初接触这个 SDK,到逐渐摸清它的运行时结构、虚拟机调度机制和算法执行流程,整个过程比预想中复杂,也收获了不少意料之外的东西。
曾经觉得非常复杂的东西,在真正投入时间、建立模型、逐步验证之后,也开始变得清晰。
比起最终还原了多少函数、多少命令,我更看重这次研究带来的分析经验。、
未知的东西总会存在,今天是 SGMAIN,明天也可能是另一套完全陌生的执行体系。
继续研究,继续积累。
他日卧龙终得雨,今朝放鹤且冲天。
不过是些许风霜罢了。

Java doCommandNative(70102, Object[])
  └ jni_doCommandNative 0x5A2FC
     └ vm_invoke(7,1,2,flag=1) 0x56F70        ← 命令调度器,不是虚拟机
        └ cmd70102_handler 0xD002C            ← 参数封送 / 结果组装
           └ vmrun 0xD3410
              └ vm_invoke(7,1,2,flag=0)       ← script 表 miss,落到 hook
                 └ g_vm_hook_fn 0x1B4DC0
                    └ 0x1B0C50 → 0x1B05C4 → 0x160300 → 0x18F6CC
                       └ 0x170D1C            ← 真虚拟机入口
0x170d58  mov  x26, x2        ; module
0x170d5c  mov  x23, x5        ; handler 表
0x170d60  mov  x27, x1        ; VM ctx
0x170d64  mov  x25, x4        ; 整数 vreg = ctx+0x40
0x170d68  mov  x28, x0        ; 函数对象
0x170d6c  add  x24, x4, #0x100 ; 浮点文件 = ctx+0x140
0x170d70  ldr  x22, [x26, #8] ; 字节码基址
0x170d74  mov  x21, x22
0x170d78  add  x21, x21, x3, lsl #4 ; 当前槽 = base + entry*0x10
0x170d7c  ldrh w0, [x21]
0x170d80  ldr  x1, [x23, x0, lsl #3]
0x170d84  str  x21, [x27, #8]
0x170d88  br   x1
jni_doCommandNative 0x5A2FC
 └ vm_invoke(7,1,2, flag=1)          0x56F70   ← native 表
    └ vm_resolve_command             0x567A8
       └ cmd70102_handler            0xD002C   ← 只做参数封送 / 结果组装
          └ vmrun                    0xD3410
             └ vm_invoke(7,1,2, flag=0)        ← script 表 miss
                └ g_vm_hook_fn 0x1B4DC0
                   └ 0x1B0C50 → 0x1B05C4 → 0x160300
                      └ 0x18F6CC → 0x170D1C   ← VM 内核
0x170d58  mov  x26, x2
0x170d5c  mov  x23, x5
0x170d60  mov  x27, x1
0x170d64  mov  x25, x4
0x170d68  mov  x28, x0
0x170d6c  add  x24, x4, #0x100   ; ctx+0x140
0x170d70  ldr  x22, [x26, #8]    ; 字节码基址
0x170d74  mov  x21, x22
0x170d78  add  x21, x21, x3, lsl #4
0x170d7c  ldrh w0, [x21]
0x170d80  ldr  x1, [x23, x0, lsl #3]
0x170d84  str  x21, [x27, #8]
0x170d88  br   x1
ANCHOR = {
    0x40A8: "ip",      # x21 字节码槽指针(控制流)
    0x40C8: "regf",    # x25 整数 vreg 文件(数据流)
    0x40B8: "table",   # x23 handler 表(边界)
    0x40C0: "float",   # x24 浮点文件(ctx+0x140)
    0x40D8: "ctx",     # x27 VM ctx
    0x40D0: "module",  # x26 module
    0x40B0: "bcbase",  # x22 字节码基址
    0x40E0: "fnobj",   # x28 函数对象
}
ldrh wN, [x21, #0x10]!     ; 取下一条 opcode,x21 前移 0x10
ldr  xN, [x23, xN, lsl #3] ; 查 handler 表
str  x21, [x27, #8]        ; 簿记当前 pc
br   xN                    ; 尾跳
0x182308  ldrb  w4, [x21, #4]
0x18230c  ldr   x3, [x25, x4, lsl #3]
0x182310  ldrsw x1, [x21, #8]
0x182314  cmp   w3, w1
0x182318  b.eq  0x182334
0x18231c  ldrsw x5, [x21, #0xc]
0x182320  add   x21, x21, x5, lsl #4   ; 落空支:相对跳
0x182324  ldrh  w8, [x21]
0x182328  ldr   x2, [x23, x8, lsl #3]
0x18232c  str   x21, [x27, #8]
0x182330  br    x2

0x182334  ldrh  w12, [x21, #0x20]!     ; 命中支:跳过 1 槽
0x182338  ldr   x11, [x23, x12, lsl #3]
0x18233c  str   x21, [x27, #8]
0x182340  br    x11
0x179d00  ldrb  w2, [x21, #6]     ; src1
0x179d04  ldr   x0, [x25, x2, lsl #3]
0x179d08  ldrb  w15, [x21, #7]    ; src2
0x179d0c  ldr   x6, [x25, x15, lsl #3]
0x179d10  orr   x12, x0, x6
0x179d14  ldrb  w14, [x21, #4]    ; dst
0x179d18  str   x12, [x25, x14, lsl #3]
0x179d1c  ldrh  w19, [x21, #0x10]!
0x179d20  ldr   x17, [x23, x19, lsl #3]
0x179d24  str   x21, [x27, #8]
0x179d28  br    x17
0x221  0x179d00  14834  orr  VREG[0x1a], VREG[0x0], VREG[0x18]   <0x2>
+0x00  u16 opcode
+0x02  .. +0x07   六个操作数(每字节一个寄存器号 / 小立即数)
+0x08  .. +0x0f   立即数 / 偏移
步长 = 0x10 × N
472143 + 105340 + 89681 + 23649 = 690813
0x186768  ldr  x11, [x25, #0x70]   ; vreg[14] = 旧 SP
0x18676c  ldr  x2,  [x21, #8]      ; 帧大小(负数)
0x186770  add  x16, x11, x2        ; 新 SP
0x186774  str  x16, [x25, #0x70]
0x186778  ldp  x9, x10, [x25, #0x80] ; vreg[16..]
0x18677c  stp  x9, x10, [x11]        ; 存到旧 SP
...
0x1867b8  ldp  x9, x10, [x25, #0x40] ; vreg[8..9]
0x1867bc  stp  x9, x10, [x25, #0xc0] ; -> vreg[24..25]
...
0x1867d0  str  x11, [x25, #0xf0]     ; vreg[30] = 旧 SP(帧基址)
0x1867d4  ldr  x19, [x25, #0x78]     ; vreg[15] = 调用点
0x1867d8  str  x19, [x25, #0xf8]     ; vreg[31] = 返回地址
0x187594  ldr  x20, [x25, #0xf0]   ; 帧基址
0x187598  ldp  x9, x10, [x25, #0xc0] ; vreg[24..]
0x18759c  stp  x9, x10, [x25, #0x40] ; -> vreg[8..]
...
0x1875b0  ldr  x14, [x25, #0xf8]   ; vreg[31] = 返回地址
0x1875b4  str  x14, [x25, #0x78]   ; -> vreg[15]
0x1875b8  ldp  x9, x10, [x20]      ; 恢复 vreg[16..31]
...
0x1875f8  str  x20, [x25, #0x70]   ; SP = 帧基址
0x1875fc  ldr  x21, [x25, #0x78]   ; vPC = 返回地址

传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

最后于 1小时前 被一只鸭子编辑 ,原因: 格式调整
收藏
点赞 ・2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回