Where it died, who calls whom, which files are yours, what may sleep, who is waiting on whom, and why one specific thing fails at one specific line. Same two words as every other page here — raised is a thing that exists, sunken is somewhere you cannot go or a channel something travels down.死在哪、谁调谁、哪些文件是你的、什么能睡眠、谁在等谁,以及某一件事为什么卡在某一行。还是这套页面一贯的两个词 —— 凸起是一个存在的东西,凹陷是去不了的地方、或者一条东西从里面走过的通道。
A backtrace read top to bottom goes from where it died back to where it started — but the stack pointer runs the other way, which is the one thing a bare list of symbols never tells you. The address gutter says it. The accent frame is the innermost one, because that is the line the reader actually came for.栈回溯从上往下读是从出事的地方回到起点 —— 但栈指针是反着走的,而一串光秃秃的符号名从来不会告诉你这件事。左边那列地址就是说这个的。强调色标的是最里面那一帧,因为读者来就是找这一行的。
Callers above, callees below, the function in question in the middle with the accent — that is the reading convention, and putting it any other way costs the reader a moment working out which half is which. Two things most call graphs leave out are marked here: a dashed edge is conditional, and a ringed node is reachable from interrupt context, which is to say nothing under it may sleep.调它的在上,它调的在下,被问的那个在中间给强调色 —— 这是读调用图的约定,反过来摆读者得先花一会儿弄清哪边是哪边。这里标了两件多数调用图会漏的事:虚线是条件调用,描圈的节点在中断上下文里会走到,也就是说它下面的一切都不许睡眠。
Three layers, and the bottom one is sunken because you must not edit it — it is the vendor drop, and the next one overwrites whatever you changed. Depth here is not decoration: a new person on the codebase needs to know which files are theirs before anything else, and this diagram answers that before they read a single name.三层,最下面一层凹下去是因为不能改 —— 那是厂商给的,下一版一来你改的全没了。这里的深度不是装饰:一个新人接手代码,第一件要知道的事就是哪些文件是自己的,这张图在他读第一个文件名之前就回答了。
Bands are contexts, ordered by what preempts what — the top band interrupts everything below it. The property the reader actually needs is can this code sleep, so the two bands where it cannot are ringed. Getting this wrong does not fail at build time; it deadlocks under load, weeks later.带是上下文,按谁打断谁排 —— 最上面那条打断它下面的一切。读者真正需要的属性是这段代码能不能睡眠,所以不能睡的那两条带描了圈。这件事搞错不会在编译时报错,会在跑满负载的时候死锁,而且是几周以后。
Each participant gets a groove running down the page — that is its lifeline, a channel through time. A raised block on the groove means that participant is on the stack right now. So the figure answers "who is waiting on whom" by shape alone: wherever two blocks overlap, the upper one is blocked.每个参与者有一条竖着贯穿页面的凹槽 —— 那是它的生命线,一条穿过时间的通道。槽上压一个凸块,表示这段时间它在栈上。所以这张图光靠形状就回答了「谁在等谁」:两个块重叠的地方,上面那个就在阻塞。
Reading one backtrace is rarely what you actually do — you compare the run that worked with the run that did not. So the frames that are the same on both sides are flattened into grooves and only the differing frames stay raised. The difference then finds you instead of the other way round.真正在做的事很少是读一个栈 —— 是拿跑通的那次和没跑通的那次比。所以两边一样的帧压平成凹槽,只有不一样的那几帧还凸着。这样是差异来找你,不是你去找差异。
The stack grows downward into a fixed allocation, so the address gutter runs the same way the memory map does. The ringed cell at the bottom is what a single oversized local array runs into. Overwriting that one leaves you with a crash that has no backtrace at all, which is why an overflow usually looks like a corrupted, unrelated panic.栈是往下长的,长进一块固定大小的空间里,所以左边那列地址和内存布局图同一个方向。底下描圈那一格,就是一个过大的局部数组会撞上的东西。踩掉它,你拿到的崩溃连回溯都打不出来 —— 所以栈溢出看起来通常像一个不相干的、内容被踩烂的 panic。
Interrupt context does not borrow the process stack — on arm64 it runs on its own, smaller one. That is the real reason a hardirq handler must stay flat: not style, not latency, but a budget that is often half the size and has nothing to do with the thread that got interrupted.中断上下文不是借用进程的栈 —— 在 arm64 上它跑在自己那块更小的栈上。这才是硬中断处理函数必须写得扁的真实原因:不是风格问题,不是延迟问题,是一块常常只有一半大、而且和被打断的那个线程毫无关系的预算。
Every driver walks this chain and almost every driver walks it by memory. Each block is a struct, the groove between them is the pointer — and the last block, the one carrying the accent, is the only one you wrote. Everything to its left is the kernel handing you back something you gave it earlier.每个驱动都要走一遍这条链,而且几乎都是凭记忆走的。每个块是一个结构体,块之间的凹槽是指针 —— 最后那个带强调色的块,是唯一一个你自己写的。它左边的一切,都是内核把你先前交给它的东西还给你。
This is the shape everything in the kernel already uses — ftrace’s function_graph, the Call trace: in an oops, gdb’s bt. Indentation is call depth, so the reader does not have to learn a legend first, and the figure lines up with the tool output still open in the other terminal. The call graph in section B answers a different question — who calls me and what do I call. This one answers what actually happened this time, in order, with the time each frame cost.内核里已经全是这个形状了 —— ftrace 的 function_graph、oops 里的 Call trace:、gdb 的 bt。缩进就是调用深度,读者不用先学图例,而且这张图能和另一个终端里还开着的工具输出逐行对上。B 节那张调用关系图回答的是另一个问题 —— 谁调我、我调谁。这一张回答的是这一次到底走了哪条路,按顺序,每一帧花了多久。
tp_read_frame() — it is a bus clock or a transfer-size question. The indentation is what makes that readable at a glance: the expensive number is four levels deep, in code you do not own.148 µs 里 93% 是一次 I2C 传输,而且那段时间线程在睡。所以要改的不是 tp_read_frame(),是总线时钟或者一次传多少字节。缩进就是让这件事一眼看出来的东西:那个大数字在第四层,在不归你管的代码里。“I changed that function and nothing happened” is usually this: the table entry points somewhere else. The table is the source, sunken because it is the text you already have; each raised block on the right is the function the slot actually lands in. An empty slot is drawn sunken and unlabelled — the kernel supplies a default, and knowing which default is the whole question.「我改了那个函数,一点反应都没有」多半就是这件事:表里那一项指到别处去了。左边那张表是源码,凹陷因为它是你已经有的原文;右边每个凸块是这一项真正落到的函数。空的槽画成凹的、不写名字 —— 内核会给一个默认实现,而是哪一个默认实现正是要问的。
.llseek left out means default_llseek(), which succeeds — so a seek on this device silently moves a file position nobody reads. That is a different bug from “seek returns an error”, and the table alone does not show it.槽留空不等于「什么都不会发生」。.llseek 没写,内核会用 default_llseek(),而它是成功返回的 —— 于是对这个设备 seek 会悄悄挪动一个没人读的文件位置。这和「seek 报错」是两种不同的 bug,光看表看不出来。The other figures on this page draw what the software looks like. This one draws why one specific thing fails at one specific line — so every level carries a file:line you can jump to, and the level that decides the outcome carries the source line itself, not a description of it. A description would lose the difference between return 0 and return -EINVAL, and that difference is usually the whole finding. A level whose line number cannot be known says why it cannot — sunken deeper, because that is what depth means everywhere else here.这一页别的图画的是软件长什么样。这一张画的是为什么某一件事卡在某一行 —— 所以每一层右边都挂着能跳过去的 file:line,而决定结局的那一层贴的是代码原文,不是对它的描述。描述会丢掉 return 0 和 return -EINVAL 的区别,而那个区别常常就是全部结论。拿不到行号的那一层要写为什么拿不到 —— 它凹得更深,因为「凹 = 够不到」在这一页从头到尾就是这个意思。
tp_fw_major = buf[0];← the only line in the driver that ever writes it← 整个驱动里唯一一次给它赋值if (tp_fw_major < 3)
return 0;← reads 0, decides the firmware is too old, returns success← 读到 0,判「固件太旧」,然后返回成功tp_gesture_enable() returns 0 — so it fails towards pass: not one line in dmesg, and double-tap-to-wake just quietly stops working. The two ringed tags are the whole coupling: one chain’s private step was the only writer of something another chain reads.被删掉的那一行和坏掉的那个功能之间,没有任何调用关系。两条链都编得过、都照跑,而 tp_gesture_enable() 返回的是 0 —— 所以它是往「放过」的方向坏:dmesg 一行都没有,双击唤醒就这么悄悄不工作了。那两枚扣子就是这整件事的耦合:一条链的私有步骤,是另一条链要读的那个东西唯一的写入方。