开了 trace:Undefined symbols _embassy_trace*
主线位置:定时器主线上执行器的每一步(spawn、入队、poll 前后、空闲)。
trace在这些位置插了七个钩子,表格和调用处见调度策略的「trace:七个由用户定义的钩子」。
flowchart TB
f["features trace"] --> x["七个 extern 声明"]
x --> d{"用户定义了吗"}
d -->|全部| ok["链接通过"]
d -->|缺几个| r{"有人引用吗"}
r -->|引用了| err["Undefined symbols"]
r -->|被优化掉| pass["碰巧通过"]
d -->|签名错| ub["通过但参数错位"]
实验 crate exp-embassy/pita-trace(platform-std、executor-thread、trace,embassy-time 开 std);M2 Max、macOS 27、rustc 1.99.0。
现象
trace 这个名字让人以为它像 log 一样会打印执行器的动作。开了它、什么都不写(bin nohooks,main 里一个永不结束的 Timer 循环),debug 构建:
error: linking with `cc` failed: exit status: 1
|
= note: Undefined symbols for architecture arm64:
"__embassy_trace_executor_idle", referenced from:
__RNvNtNtCskElGv21JdXs_16embassy_executor3raw5trace13executor_idleB5_ in libembassy_executor-e5286d2c5a0a781e.rlib[4](embassy_executor-e5286d2c5a0a781e.embassy_executor.f0885d27e806aec4-cgu.0.rcgu.o)
"__embassy_trace_poll_start", referenced from:
...
"__embassy_trace_task_end", referenced from:
__RNvNtNtCskElGv21JdXs_16embassy_executor3raw5trace8task_endCsjO4uorhKS6m_7nohooks in nohooks-d4079991d0554a2d.91xexlpfvhscqjp5m5so6hq0f.1hzqpml.rcgu.o
"__embassy_trace_task_exec_begin", referenced from:
...
"__embassy_trace_task_exec_end", referenced from:
...
"__embassy_trace_task_new", referenced from:
...
"__embassy_trace_task_ready_begin", referenced from:
...
ld: symbol(s) not found for architecture arm64
缺几个不是固定的。同一组程序在 debug 和 release 下缺的符号:
| bin | 定义了几个钩子 | 程序 | debug | release |
|---|---|---|---|---|
nohooks | 0 | main 永不结束 | 缺 7 个 | 缺 6 个(没有 task_end) |
six | 6(没有 task_end) | main 打印一行后 exit | 缺 task_end | 链接通过,打印 six hooks are enough here |
six_end | 6(没有 task_end) | 多一个会返回的任务 | 缺 task_end | 缺 task_end |
_embassy_trace_task_end 在泛型的 TaskStorage::<F>::poll 里、future 返回 Ready 的分支上调用。没有任何任务能结束时,release 把这个分支整个删掉,引用随之消失;debug 不做这种优化。于是「只写六个钩子」的程序可能 release 能编、debug 编不过,或者今天能编、加了一个会结束的任务之后编不过。
签名写错时什么也不报。wrong_sig 定义了全部七个,只是把 _embassy_trace_task_new 写成一个参数:
#[unsafe(no_mangle)]
fn _embassy_trace_task_new(task_id: u32) {
println!("task_new got {task_id:#x}");
}
task_new got 0x6fd81658
poll_start executor_id = 0x6fd81658
exec_begin task_id = 0xd0d70
编译、链接、运行都没有警告。task_new 收到的「task id」其实是执行器的 id(和 poll_start 打出的一样),真正的任务 id 是 0xd0d70。
出处
trace.rs 在 trace feature 下声明七个外部函数,模块文档写的是「All callbacks must be implemented」:
#[cfg(feature = "trace")]
unsafe extern "Rust" {
// ...
fn _embassy_trace_poll_start(executor_id: u32);
// ...
fn _embassy_trace_task_new(executor_id: u32, task_id: u32);
// ...
fn _embassy_trace_task_end(executor_id: u32, task_id: u32);
// ...
}
用户用 #[unsafe(no_mangle)] 导出同名函数,链接器按名字把两边接上,和时间驱动是同一个办法(Undefined symbols: _embassy_time_now)。不同的是时间驱动的定义由 time_driver_impl! 宏生成,签名不会错;钩子是手写的,no_mangle 符号只有名字,不带类型,rustc 不会检查两边的参数是否一致,extern "Rust" 也没有跨 crate 的签名校验。参数按调用约定从寄存器里取,少写一个参数,第一个参数拿到的就是调用方的第一个参数 executor_id。
task_end 的包装是 #[inline] 的,调用处在每个任务各自单态化出来的 poll 里,所以目标文件名里出现的是用户 crate(nohooks-...rcgu.o),其余六个在 libembassy_executor 里。
修法
七个都定义,参数和声明一致(bin seven,只统计 spawn、结束和 poll 次数):
#[unsafe(no_mangle)]
fn _embassy_trace_poll_start(_executor_id: u32) {}
#[unsafe(no_mangle)]
fn _embassy_trace_executor_idle(_executor_id: u32) {}
#[unsafe(no_mangle)]
fn _embassy_trace_task_new(_executor_id: u32, _task_id: u32) {
NEW.fetch_add(1, Relaxed);
}
#[unsafe(no_mangle)]
fn _embassy_trace_task_end(_executor_id: u32, _task_id: u32) {
END.fetch_add(1, Relaxed);
}
#[unsafe(no_mangle)]
fn _embassy_trace_task_ready_begin(_executor_id: u32, _task_id: u32) {}
#[unsafe(no_mangle)]
fn _embassy_trace_task_exec_begin(_executor_id: u32, _task_id: u32) {
POLLS.fetch_add(1, Relaxed);
}
#[unsafe(no_mangle)]
fn _embassy_trace_task_exec_end(_executor_id: u32, _task_id: u32) {}
once done
new 2 end 1 polls 3
两个任务(main 和 once)、一个结束、三次 poll(once 一次,main 在 Timer 前后各一次)。钩子可能在中断里被调用(task_ready_begin 来自 wake),芯片上要用原子量或临界区,不能拿锁。
执行器没有「把调度过程打印出来」的开关:它的 log / defmt feature 只给内部 fmt.rs 里的宏选后端,而 fmt.rs 之外的 src/ 里一句 trace! / debug! / info! 都没调用。trace 的用处是自己量东西,比如一个任务忙等,其他任务全停用 exec_begin / exec_end 抓出 poll 太久的任务。
怎么确认没中招
- debug 和 release 各链接一次。只在 release 下测过的钩子集合,debug 下可能缺
task_end。 - 七个钩子放在一个文件里,签名对照
trace.rs的extern块逐字抄,参数名可以加下划线,个数和类型不能变。 - 一个简单的自检:在
task_new和poll_start里都打印收到的第一个参数,两者应当相等(都是executor_id);task_new的第二个参数应当等于SpawnToken::id()。
小结
trace不打印东西,它要求用户定义七个_embassy_trace_*函数;少了就是Undefined symbols。- 缺几个取决于程序和优化级别:
task_end只在有任务能结束、或 debug 构建时才被引用。 - 签名写错不报错,参数会错位;照
trace.rs的声明一个不差地写全七个。