WarmGrayMatteInkMistClaySage
RELIEF · CODE

Twelve ways to draw software画软件的十二种画法

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 · Call stackA · 函数堆栈

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.栈回溯从上往下读是从出事的地方回到起点 —— 但栈指针是反着走的,而一串光秃秃的符号名从来不会告诉你这件事。左边那列地址就是说这个的。强调色标的是最里面那一帧,因为读者来就是找这一行的。

BACKTRACE · touch driver栈回溯 · 触摸驱动addresses grow downward here, so the frame printed first sits at the highest address这里地址是往下增长的,所以先打印出来的那一帧在最高地址
0xffffffc0_11a4
my_tp_report()drivers/input/tp_core.c:412
where it died出事的地方
0xffffffc0_11e8
tp_irq_thread()drivers/input/tp_core.c:508
caller调它的
0xffffffc0_1240
irq_thread_fn()kernel/irq/manage.c
0xffffffc0_1298
irq_thread()kernel/irq/manage.c
0xffffffc0_12f0
kthread()kernel/kthread.c
0xffffffc0_1338
ret_from_fork()arch/arm64/kernel/entry.S
bottom of stack栈底

B · Call graphB · 函数调用关系

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.调它的在上,它调的在下,被问的那个在中间给强调色 —— 这是读调用图的约定,反过来摆读者得先花一会儿弄清哪边是哪边。这里标了两件多数调用图会漏的事:虚线是条件调用,描圈的节点在中断上下文里会走到,也就是说它下面的一切都不许睡眠。

tp_read_frame() · who calls it, what it callstp_read_frame() · 谁调它,它调谁the ringed pair is why this function cannot be called from the hardirq half描圈那两个,就是这个函数不能在硬中断那一半里调的原因
tp_irq_thread()threaded irq · may sleep中断线程 · 可以睡眠
tp_resume()pm callback电源回调
tp_open()first user opens the device第一个用户打开设备
tp_read_frame()the function this figure is about这张图讲的就是它
i2c_transfer()sleeps — never call from hardirq会睡眠 —— 硬中断里不能调
crc16()pure纯计算
input_report_abs()
tp_recover()only on checksum failure只在校验失败时走

C · Code architectureC · 代码架构

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.三层,最下面一层凹下去是因为不能改 —— 那是厂商给的,下一版一来你改的全没了。这里的深度不是装饰:一个新人接手代码,第一件要知道的事就是哪些文件是自己的,这张图在他读第一个文件名之前就回答了。

TOUCH DRIVER · file layout触摸驱动 · 文件分层a bug in the sunken layer is a ticket to the vendor, not a patch凹那层里的 bug 是给厂商开单,不是自己打补丁
Facing the kernel对上:给内核看的一面 what other subsystems call别的子系统调的是这些
tp_probe()registers everything注册全部资源
file_operationsopen · read · ioctl
input_devreports touches上报触摸点
pm_opssuspend · resume
Core logic — ours核心逻辑:我们写的 the only files a bug report should send you to出问题该翻的就是这几个文件
tp_core.cstate machine, frame parsing状态机、帧解析
tp_fw.cfirmware update固件升级
tp_gesture.cwake gestures手势唤醒
tp_debug.csysfs, register dumpsysfs、寄存器 dump
Facing the hardware — vendor code对下:厂商代码 read it, do not edit it — a vendor drop overwrites the lot只读不改 —— 厂商下一版一来全部覆盖
tp_hal_i2c.cvendor · read only厂商 · 只读
tp_reg_map.hvendor · read only厂商 · 只读
tp_fw_bin.hvendor blob厂商二进制

D · Execution contextsD · 运行与调度关系

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.带是上下文,按谁打断谁排 —— 最上面那条打断它下面的一切。读者真正需要的属性是这段代码能不能睡眠,所以不能睡的那两条带描了圈。这件事搞错不会在编译时报错,会在跑满负载的时候死锁,而且是几周以后。

WHO RUNS WHERE谁在什么上下文里跑the same function body is legal in one band and a deadlock in another同一段函数体,在这条带里合法,在那条带里就是死锁
Hard IRQ context硬中断上下文cannot sleep · preempts everything below不能睡眠 · 打断下面所有的
tp_irq_handler()reads the status byte, wakes the thread读一个状态字节,唤醒线程
Soft IRQ / tasklet软中断 / taskletstill cannot sleep照样不能睡眠
net_rx_action()
timer softirq
Process context进程上下文may sleep — i2c, mutex, kmalloc(GFP_KERNEL) are all fine here可以睡眠 —— i2c、mutex、kmalloc(GFP_KERNEL) 在这里都没问题
tp_irq_thread()the threaded half of the irq中断的线程那一半
workqueuetp_fw_update_work
read() syscalla user process asking for data用户进程来要数据

E · Sequence diagramE · 函数时序图

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.每个参与者有一条竖着贯穿页面的凹槽 —— 那是它的生命线,一条穿过时间的通道。槽上压一个凸块,表示这段时间它在栈上。所以这张图光靠形状就回答了「谁在等谁」:两个块重叠的地方,上面那个就在阻塞。

ONE read() FROM APP TO CHIP一次 read() 从应用到芯片the accent block is the one holding the mutex the whole way down强调色那一块,是全程握着那把锁的那个
read()the app应用
VFS
tp_core.c
i2c bus
TP chip
read(fd, buf, 16)read(fd, buf, 16)
.read = tp_read().read = tp_read()
i2c_transfer()i2c_transfer()
START · addr · regSTART · 地址 · 寄存器
16 bytes16 字节
returns — this is where it slept返回 —— 睡眠发生在这里
copy_to_user()copy_to_user()

F · Two stacks, side by sideF · 两个栈并排比

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.真正在做的事很少是读一个栈 —— 是拿跑通的那次和没跑通的那次比。所以两边一样的帧压平成凹槽,只有不一样的那几帧还凸着。这样是差异来找你,不是你去找差异。

SAME DRIVER, TWO RUNS同一个驱动,两次运行three extra frames, and the top one is a mutex — that is the whole finding多出来三帧,最上面那帧是个 mutex —— 结论就这一句
the run that worked跑通的那一次
i2c_transfer()
tp_read_frame()
tp_irq_thread()
irq_thread_fn()
irq_thread()
kthread()
ret_from_fork()
the run that hung卡死的那一次
__mutex_lock()extra多出来的
i2c_transfer()
tp_recover()extra多出来的
tp_read_frame()
tp_irq_thread()
irq_thread_fn()
irq_thread()
kthread()
ret_from_fork()

G · Stack overflowG · 栈溢出

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。

A 2 KB LOCAL IN A 16 KB STACK16 KB 的栈里一个 2 KB 的局部变量CONFIG_FRAME_WARN catches the single big frame; it does not catch a deep chain of small onesCONFIG_FRAME_WARN 抓得住单个大帧,抓不住一长串小帧
0xffff…c000
stack top — frames grow downward from here栈顶 —— 帧从这里往下长
0xffff…a100
tp_fw_parse()a 2 KB local array — a quarter of the whole stack in one frame一个 2 KB 的局部数组 —— 一帧吃掉整个栈的四分之一
0xffff…9c80
blk_mq_submit_bio()
0xffff…9a00
ext4_writepages()
0xffff…8040
thread_infooverwrite this and the crash has no backtrace at all踩到它,崩溃时连回溯都打不出来

H · Two stacks, two budgetsH · 中断栈和进程栈

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 上它跑在自己那块更小的栈上。这才是硬中断处理函数必须写得扁的真实原因:不是风格问题,不是延迟问题,是一块常常只有一半大、而且和被打断的那个线程毫无关系的预算。

WHERE A HANDLER ACTUALLY RUNS中断处理函数到底跑在哪a deep call from a hardirq overflows a stack the process side never sees从硬中断里往深里调,撑爆的是进程那边根本看不见的一块栈
process stack · 16 KB进程栈 · 16 KB
syscall entry
vfs_read()
tp_read()
i2c_transfer()
...room for deep chains够深的调用链用
irq stack · often 8 KB or less中断栈 · 常常只有 8 KB 甚至更少
el1_irq
gic_handle_irq()
tp_irq_handler()keep it flat here这里必须扁

I · Where the private pointer came fromI · 私有指针是从哪来的

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.每个驱动都要走一遍这条链,而且几乎都是凭记忆走的。每个块是一个结构体,块之间的凹槽是指针 —— 最后那个带强调色的块,是唯一一个你自己写的。它左边的一切,都是内核把你先前交给它的东西还给你。

dev → platform_device → drvdata → yoursdev → platform_device → drvdata → 你的get this wrong and you get a pointer that is valid, wrong, and silent这条链走错,你得到的是一个合法的、错的、而且不报错的指针

struct device *dev

probe()'s argumentprobe() 的参数
bus
platform
driver_data
→

platform_device

container_of
dev
id
-1
resource
→

drvdata

set by
platform_set_drvdata()
→ your private struct→ 你的私有结构

struct tp_data

client
i2c_client *
input
input_dev *
lock
mutex

J · One trace, indentedJ · 一次执行,缩进着看

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 节那张调用关系图回答的是另一个问题 —— 谁调我、我调谁。这一张回答的是这一次到底走了哪条路,按顺序,每一帧花了多久。

ftrace · one touch interruptftrace · 一次触摸中断the indent is the depth; the number on the right is where the time went缩进是深度,右边那个数说明时间花在哪了
tp_irq_thread()drivers/input/tp_core.c:508148.3 µs
mutex_lock()kernel/locking/mutex.c0.9 µs
tp_read_frame()the one you came here for你要找的就是它141.6 µs
i2c_transfer()drivers/i2c/i2c-core-base.c138.2 µs
rk_i2c_xfer()bus driver · 100 kHz总线驱动 · 100 kHz137.4 µs
wait_for_completion_timeout()sleeps until the controller is done睡到控制器做完136.1 µs
tp_parse_points()pure arithmetic纯算术2.1 µs
tp_recalibrate()not entered — the drift threshold was not crossed没进去 —— 漂移没到阈值—
input_report_abs()drivers/input/input.c1.4 µs
mutex_unlock()0.3 µs
93% of the 148 µs is one I2C transfer, and inside it the thread is asleep. So the fix is not in 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(),是总线时钟或者一次传多少字节。缩进就是让这件事一眼看出来的东西:那个大数字在第四层,在不归你管的代码里。

K · Where a pointer in a table actually goesK · 表里那个指针到底跳到哪

“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.「我改了那个函数,一点反应都没有」多半就是这件事:表里那一项指到别处去了。左边那张表是源码,凹陷因为它是你已经有的原文;右边每个凸块是这一项真正落到的函数。空的槽画成凹的、不写名字 —— 内核会给一个默认实现,而是哪一个默认实现正是要问的。

struct file_operations · slot by slotstruct file_operations · 逐项看去向grep the slot name, not the function name — the function name is not what the kernel calls要 grep 的是槽名不是函数名 —— 内核喊的不是函数名
/* drivers/input/tp_core.c */ static const struct file_operations tp_fops = { .owner = THIS_MODULE, .open = tp_open, .read = tp_read, .unlocked_ioctl = tp_ioctl, /* .llseek not set *//* .llseek 没写 */ .release = tp_release, };
the table, as written表的原文
.open→ tp_open()tp_core.c:96
.read→ tp_read()tp_core.c:141
tp_read_frame()the function you edited你改的是这个tp_core.c:412
.unlocked_ioctl→ tp_ioctl()tp_core.c:203
.llseekleft NULL → the kernel uses default_llseek()留空 → 内核走 default_llseek()fs/read_write.c
.release→ tp_release()tp_core.c:118
where each slot lands每一项的去向
A NULL slot is not “nothing happens”. .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,光看表看不出来。

L · Why it fails at that lineL · 为什么卡在这一行

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 的区别,而那个区别常常就是全部结论。拿不到行号的那一层要写为什么拿不到 —— 它凹得更深,因为「凹 = 够不到」在这一页从头到尾就是这个意思。

ONE LINE DELETED, ONE FEATURE GONE删掉一行,坏掉一个功能the two chains never call each other — all they share is the static variable on the two ringed tags两条链彼此不调用 —— 它们共用的只有那两枚扣子上的静态变量
BOOT · probe runs this once开机 · probe 里跑一次
tp_fw_init()called once from probeprobe 调它一次drivers/input/tp_fw.c:27
tp_fw_version_read()removed删掉了tp_fw_majorlooked like it only fed the debug print看上去只是给那行调试打印用的tp_fw.c:31
tp_fw.c:23tp_fw_major = buf[0];← the only line in the driver that ever writes it← 整个驱动里唯一一次给它赋值
RESUME · the chain that broke唤醒 · 坏掉的那条链
PM coreno line to give · dispatched at runtime through dev_pm_ops给不出行号 · 运行时按 dev_pm_ops 分派
tp_resume()the .resume slot of tp_pm_opstp_pm_ops 的 .resumedrivers/input/tp_pm.c:16
tp_gesture_enable()tp_fw_majorthe line you came here for你要找的就是这一行tp_pm.c:24
tp_fw.c:41-42if (tp_fw_major < 3) return 0;← reads 0, decides the firmware is too old, returns success← 读到 0,判「固件太旧」,然后返回成功
tp_write_reg()never reached — the write that arms double-tap没走到 —— 真正打开双击唤醒的那次写tp_fw.c:44
0so the dev_warn() two lines below never fires所以下两行那句 dev_warn() 永远不打
There is no call path between the line that was deleted and the feature that broke. Both chains compile, both run, and 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 一行都没有,双击唤醒就这么悄悄不工作了。那两枚扣子就是这整件事的耦合:一条链的私有步骤,是另一条链要读的那个东西唯一的写入方。