WarmGrayMatteInkMistClaySage
RELIEF · STRUCTS

The code is on the left of every figure每张图左边都贴着它的代码

A diagram of a data structure earns its place only if the reader can point at the line it came from. So on this page the source sits in a sunken panel beside each figure — the code is the original, the figure is the reading of it, and the mapping is done by the eye instead of by memory.一张画数据结构的图,只有在读者能指着它对应的那行代码时才值得存在。所以这一页源码就放在每张图旁边的凹陷面板里 —— 代码是原文,图是对原文的解释,而这层对应关系用眼睛就完成了,不用靠记。

1 · What a struct actually occupies1 · 结构体到底占了多少

The fields you wrote are on the left, what the compiler laid out is on the right — and the padding is drawn as sunken gaps, because that is what it is: nothing. Two holes here cost 15 of 56 bytes. The dashed line is the cache line boundary, which is the reason this matters beyond tidiness.你写的字段在左边,编译器排出来的在右边 —— 而填充画成凹陷的空格,因为它就是这个:什么都没有。这里两个空洞吃掉了 56 字节里的 15 个。那条虚线是 cache line 边界,也是这件事不只是「整不整齐」的原因。

struct tp_data · 56 bytes, 15 wastedstruct tp_data · 56 字节,浪费 15 个pahole prints these numbers; the figure is for seeing which two moves fix thempahole 会打出这些数字,图是用来看清「挪哪两下能修好」的
/* drivers/input/tp_core.c */ struct tp_data { bool present; u64 last_ts; u8 lanes; struct mutex lock; struct i2c_client *client; char name[16]; }; /* $ pahole -C tp_data tp_core.o */ /* size: 72, holes: 2, sum holes: 14 */
Move lanes up next to present: the two holes become one 6-byte hole, 72 bytes becomes 64, and the struct lands on exactly one cache line — name stops straddling.把 lanes 挪到 present 旁边:两个空洞并成一个 6 字节的,72 字节变 64,结构体正好落在一条 cache line 上 —— name 不再跨界。
0
presentbool
1 B
1
—padding the compiler inserted编译器填的空洞
7 B
8
last_tsu64
8 B
16
lanesu8
1 B
17
—padding the compiler inserted编译器填的空洞
7 B
24
lockstruct mutex
24 B
48
clientstruct i2c_client *
8 B
56
name[16]char
16 B
cache line 64 B

2 · Three relations that look alike in code2 · 代码里长得像的三种关系

struct B b, struct B *b and struct list_head n are one character apart in the source and three different lifetimes in reality. Embedding is drawn as containment, a pointer as a hop out to somewhere else, and a list_head as neither — because the list holds the head, not your struct.struct B b、struct B *b、struct list_head n 在源码里差一个字符,在现实里是三条不同的生命周期。嵌入画成包含,指针画成跳到别处,而 list_head 两者都不是 —— 因为链表串的是那个 head,不是你的结构体。

3 · Where your private pointer is parked3 · 你的私有指针存在哪

Four lines of probe, drawn. The kernel hands you a struct device; you allocate your own struct and then park a pointer to it on the kernel’s side so that every later callback can get it back. The chain is short — what makes it worth a figure is that it is walked from memory every single time.probe 里的四行,画出来。内核给你一个 struct device;你分配自己的结构体,然后把指向它的指针寄存在内核那边,让之后每一次回调都能取回来。链很短 —— 值得画成图,是因为它每次都是凭记忆走的。

device → client → yoursdevice → client → 你的get the direction backwards and you get a pointer that is valid, wrong, and silent方向弄反,你得到的是一个合法的、错的、而且不报错的指针
/* these four lines of probe are the picture below *//* probe 里这四行,就是下面那张图 */ static int tp_probe(struct i2c_client *cl) { struct tp_data *tp; tp = devm_kzalloc(&cl->dev, sizeof(*tp), GFP_KERNEL); tp->client = cl; /* remember it *//* 往下记 */ i2c_set_clientdata(cl, tp); /* hang yours on it *//* 往上挂 */ ... } /* elsewhere, to get it back: *//* 别处要拿回来: */ struct tp_data *tp = i2c_get_clientdata(cl);
The two orange lines are the round trip: set_clientdata stores your struct on the kernel’s side, get_clientdata takes it back. Everything else in the chain the kernel already built for you.橙色那两行就是这一来一回:set_clientdata 把你的结构体存到内核那边,get_clientdata 再取回来。链上其余的部分,内核早就替你搭好了。

struct device

the kernel’s内核那边的
dev
driver_data
void *

struct i2c_client

contains内含
struct device dev
addr
0x38

struct tp_data

client
points back指回去
lock
mutex
input
input_dev *

4 · From a device tree property to a struct field4 · 从设备树属性到结构体字段

Each row is one property and the field it becomes, with the function that does the conversion named on the row itself — because that name is what you actually go looking for. The last row is sunken on purpose: compatible never becomes a field at all.每一行是一个属性和它变成的那个字段,而且做这次转换的函数就写在这一行上 —— 因为你真正要找的就是那个函数名。最后一行故意是凹的:compatible 根本不会变成任何字段。

ONE NODE, THREE PROPERTIES一个节点,三个属性a typo in compatible is silent; a typo in quartz,lanes gives you a zero — two very different bugscompatible 写错是一片安静,quartz,lanes 写错给你一个 0 —— 两种完全不同的 bug
/* arch/arm64/boot/dts/... */ touch@38 { compatible = "quartz,tp-q1"; reg = <0x38>; quartz,lanes = <4>; reset-gpios = <&gpio4 18 GPIO_ACTIVE_LOW>; }; /* read in probe *//* probe 里读出来 */ of_property_read_u32(np, "quartz,lanes", &tp->lanes); tp->rst = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
Note the last row: compatible is the one property that never becomes a field. It is consumed before your code runs, which is why a typo there produces silence rather than a wrong value.注意最后一行:compatible 是唯一一个永远不会变成字段的属性。它在你的代码跑起来之前就被消费掉了 —— 所以那里写错得到的是一片安静,不是一个错的值。
dts
quartz,lanes = <4>of_property_read_u32()of_property_read_u32()
u32
→
tp->lanes = 4
1 B
dts
reset-gpios = <…>devm_gpiod_get(dev, "reset", …)devm_gpiod_get(dev, "reset", …)
—
→
tp->rst
8 B
dts
compatibleof_match_tableof_match_table
—
×
—never reaches your struct — it only decides whether probe runs根本到不了你的结构体 —— 它只决定 probe 跑不跑
—

5 · Bitfields, and what __packed does to them5 · 位域,以及 __packed 对它做了什么

Two things the source does not show. First, bitfields are cut out of a storage unit the compiler picks, filling from the least significant bit on a little-endian target — so mode is bits 2:0, not the top of the byte. Second, __packed does not just make the struct smaller: it moves every later field, and both of the ones after the bitfield land off their natural boundary — the u16 at offset 1, the u32 at offset 3. On arm64 that read is fixed up in a trap handler; on some 32-bit cores it is a fault. The figure puts both views under the source: the bit strip for the unit, the byte list for what moved.源码上看不出来的两件事。第一,位域是从编译器挑的一个存储单元里切出来的,小端目标上从最低位开始填 —— 所以 mode 是 bit 2:0,不是那个字节的高位。第二,__packed 不只是把结构体变小:它会把后面每个字段都往前挪,位域后面那两个字段都落在了非自然边界上 —— u16 在偏移 1,u32 在偏移 3。arm64 上这次读会掉进处理非对齐访问的陷阱里,有些 32 位核上直接是异常。这张图把两个视角都摆在源码下面:位带看单元怎么切,字节表看什么挪了位置。

struct tp_cfg · with and without __packedstruct tp_cfg · 加不加 __packedone byte saved, two fields left unaligned — the byte list is generated from pahole, not typed by hand省下一个字节,换来两个字段非对齐 —— 右边那张字节表是 pahole 生成的,不是手敲的
/* drivers/input/tp_core.h */ struct tp_cfg { u8 mode : 3; /* bit 2:0 */ u8 lanes : 2; /* bit 4:3 */ u16 rate; u32 window; } __packed; /* $ pahole -C tp_cfg tp_core.o */ /* __packed: size 7, rate @ 1, window @ 3 */ /* unpacked: size 8, rate @ 2, window @ 4 *//* 不加 __packed: size 8, rate @ 2, window @ 4 */
the source, as written源码原文
7·5
4·3
2·0
—
lanes2 b
mode3 b
byte 0 · filled from bit 0 up第 0 字节 · 从 bit 0 往上填
0
mode / lanes5 bits used, 3 left over用了 5 位,剩 3 位
1 B
1
rate16-bit field at offset 1 — unaligned16 位字段落在偏移 1 —— 非对齐
2 B
3
window32-bit field at offset 3 — unaligned32 位字段落在偏移 3 —— 非对齐
4 B
with __packed — total 7 B加了 __packed —— 共 7 B
0 + 1 + 2 = 3, and 3 + 4 = 7. Without __packed the compiler inserts one pad byte at offset 1, rate lands at 2 and window at 4, and the struct is 8 bytes. You trade that one byte for an unaligned access on every read of both — and the compiler will not warn, because you asked for it. Every number here comes out of pahole; none of it is typed by hand.0 + 1 + 2 = 3,3 + 4 = 7。不加 __packed 时编译器会在偏移 1 塞一个填充字节,rate 落在 2、window 落在 4,结构体 8 字节。你用那一个字节换来的是此后每次读这两个都非对齐 —— 而且编译器不会警告,因为是你自己要的。这里每个数字都出自 pahole,没有一个是手敲的。

6 · A union is not a struct with fewer bytes6 · 联合体不是「字段少一点的结构体」

An offset list cannot draw a union, because an offset list assumes one field comes after another. Here every member starts at byte 0 and they sit on top of each other. That is the whole point and it is also the whole danger: writing one member and reading another does not convert anything, it re-reads the same bytes under a different type. The bar that is longest sets the size; the tail nobody covers is still allocated.偏移表画不出联合体,因为偏移表的前提是「一个字段接着一个字段」。这里每个成员都从第 0 字节开始,互相压在一起。这既是它的全部用处,也是它的全部风险:写了一个成员再读另一个,不会做任何转换,只是换个类型把同一批字节再读一遍。最长的那根决定大小;没人盖到的尾巴照样占着内存。

union tp_arg · all four start at byte 0union tp_arg · 四个都从第 0 字节开始the bytes are shared; only the reading rule changes字节是共用的,变的只是解读规则
/* an ioctl argument *//* 一个 ioctl 的参数 */ union tp_arg { u32 raw; struct { u16 x, y; } point; u8 bytes[4]; u64 stamp; }; /* after writing raw = 0x0002_0001, little-endian *//* 小端上写 raw = 0x0002_0001 之后 */ /* point.x == 1, point.y == 2 */ /* big-endian: the reverse *//* 大端上正好反过来 */
the source, as written源码原文
byte字节
0
1
2
3
4
5
6
7
raw
u324 B
not covered没盖到
point
xu16
yu16
not covered没盖到
bytes[4]
u8 × 44 B
not covered没盖到
stamp
u648 B — this one sets the size8 B —— 大小由它决定
sizeof(union tp_arg) == 8sizeof(union tp_arg) == 8
The bug this figure is about: user space fills point (4 bytes) and the driver reads stamp (8 bytes). Bytes 4–7 were never written, so the driver reads whatever was on the stack — and it reads it successfully, every time, with a different value. Copy the union in full or check which member the ioctl number promised; the type system will not do it for you.这张图讲的那个 bug:用户态填的是 point(4 字节),驱动读的是 stamp(8 字节)。第 4–7 字节从来没被写过,驱动读到的是栈上残留的东西 —— 而且每次都读成功,每次值还不一样。要么整个联合体拷全,要么按 ioctl 号确认该读哪个成员;类型系统不会替你挡。