事情是这样的,最近收到一个 PR https://github.com/dying-will-bullet/deunicode/pull/2,非常感谢 Ethan Evans 。这个 PR 是升级 Zig 版本到 0.15.1 的,改动点是 Zig 那些 break change 的 API。但是在合并之后我发现这个改动在 Zig 0.15.2 是会单元测试失败的。查了一下发现这个问题还是比较有意思的
环境#
复现问题的环境如下:
- 代码 commit
63051c6 - Zig 0.15.1 / 0.15.2 / v0.16.0-dev.1484+d0ba6642b 。可以使用
mise来切换 Zig 版本 - LLVM 21.1.6
- Linux 6.17.8-arch1-1 x86_64 unknown amd64
现象描述#
这个代码逻辑比较简单,就是查表然后做 deunicode,这代码本身也是从 Rust 版本移植过来的。让我们直接切入到主题里面,首先使用 Zig 0.15.1 版本
现象一: 在存在 std.debug.assert(custom_placeholder.len > 0); 这行代码的时候,执行 zig build test 是 All passed 的。但是如果移除这一行,则单元测试会失败
if (res == null) {
std.debug.assert(custom_placeholder.len > 0); // 这行
try out.appendSlice(custom_placeholder);
continue;
}
现象二: 更加神奇的是,在移除 assert 这一行并且将下面两行反注释掉,试图通过 print 了解我们的返回结果的时候,我们的单元测试又会通过
// std.debug.print("{any}\r\n", .{res});
// std.debug.print("{any}\r\n", .{expect});
现象三: 在移除 assert 这一行并且将下面被注释的所有测试用例全部反注释之后,我们所有的单元测试都会成功!
现象四: 即使不是 assert ,我们增加 print 这种不会被编译优化掉的语句在其他的位置,也会导致测试成功通过
现象五:以上所有的现象在 aarch64 均无法复现,但是在 amd64 下可以复现。win 下不确定,因为我没有 windows
现象六: 升级到 Zig 0.15.2 或者当前的 master 后,不管怎样测试都会失败
根据以上六个现象,我们可以发现
当我们试图修改代码增加调试行为的时候,bug 似乎会消失
不同平台行为不一致
我们跑单个测试用例也存在失败的情况,那么这个和 race condition 无关
单个测试用例会失败,多个测试用例则会成功。那么大概率说明并不是 buffer 被复用,没有清理导致的
似乎这个 bug 和我们
.text段的内容有关。也有可能是 stack 上预留的空间长度有关,因为增加assert或者print之后,会涉及新的栈上的变量空间预留,导致sub的值增大疑似和编译器行为有关系
Debug#
对于这种奇怪的问题,最好的方式是直接 GDB 进去调试
下文中引用两个 binary 来进行对比,编译器版本是 Zig 0.15.1
./.zig-cache/o/10c1e6a7582bccf5604417e043694e46/test失败的./.zig-cache/o/4956a4125b6989c9d0d9954375054576/test成功的
我们将失败的单元测试的 binary 通过 GDB 进行调试。132 行这里对应 if (res == null) {
Reading symbols from ./.zig-cache/o/10c1e6a7582bccf5604417e043694e46/test...
(gdb) break src/lib.zig:131
Breakpoint 1 at 0x10359e7: file lib.zig, line 132.
(gdb) r
Starting program: /home/kumiko/deunicode/.zig-cache/o/10c1e6a7582bccf5604417e043694e46/test
[New LWP 358477]
Thread 1 "test" hit Breakpoint 1, lib.deunicodeCustomAlloc (allocator=..., s=..., custom_placeholder=...) at lib.zig:132
132 if (res == null) {
(gdb) print res
$1 = {has_value = 0, null = <incomplete type>, ? = {ptr = 0x0, len = 0}}
而成功的单元测试的 binary 通过 GDB 进行调试
Reading symbols from ./.zig-cache/o/4956a4125b6989c9d0d9954375054576/test...
(gdb) break src/lib.zig:131
Breakpoint 1 at 0x10359e7: file lib.zig, line 132.
(gdb) r
Starting program: /home/kumiko/deunicode/.zig-cache/o/4956a4125b6989c9d0d9954375054576/test
[New LWP 363772]
Thread 1 "test" hit Breakpoint 1, lib.deunicodeCustomAlloc (allocator=..., s=..., custom_placeholder=...) at lib.zig:132
132 if (res == null) {
(gdb) print res
$1 = {has_value = 18972131, null = <incomplete type>, ? = {ptr = 0x1217de3 <__anon_4870+594> "AE\002C", len = 2}}
简单对比一下首次断点触发后的 res 变量的值,我们发现在失败的情况下我们的 res == null。会走到我们 if 的分支里面然后 appendSlice
if (res == null) {
std.debug.assert(custom_placeholder.len > 0);
try out.appendSlice(custom_placeholder);
continue;
}
得到
(gdb) print out
$6 = {items = {ptr = 0x7ffff6fc0000 "[?]", '\252' <repeats 12 times>, len = 3},
capacity = 15, allocator = {ptr = 0x11eed10 <testing.allocator_instance>,
vtable = 0x11f69e0 <__anon_1857>}}
这里 [?] 字符是我们的 custom_placeholder 的变量的值,这就导致了我们最终的结果是和 expect 不同的。那么为什么我们的 res 在这里会是 null 呢。我们向上来阅读代码
const res = if (has_next_cache) blk: {
break :blk next_cache;
} else blk: {
break :blk getReplacement(codepoint.?);
};
对于一个函数来说,如果它没有副作用(纯函数),一致的输入总会得到一致的输出的。通过断点,我们可以发现 codepoint 的值是正常被迭代出来的。也就是说我们的输入是一致的
Reading symbols from ./.zig-cache/o/10c1e6a7582bccf5604417e043694e46/test...
(gdb) break src/lib.zig:122
Breakpoint 1 at 0x10358ef: file lib.zig, line 122.
(gdb) r
Starting program: /home/kumiko/deunicode/.zig-cache/o/10c1e6a7582bccf5604417e043694e46/test
[New LWP 467141]
Thread 1 "test" hit Breakpoint 1, lib.deunicodeCustomAlloc (allocator=..., s=...,
custom_placeholder=...) at lib.zig:122
warning: Source file is more recent than executable.
122 const res = if (has_next_cache) blk: {
(gdb) p/x *(int*)&codepoint
$1 = 0xc6
(gdb) c
Continuing.
[0/1] Test
Thread 1 "test" hit Breakpoint 1, lib.deunicodeCustomAlloc (allocator=..., s=..., custom_placeholder=...) at lib.zig:122
122 const res = if (has_next_cache) blk: {
(gdb) p/x *(int*)&codepoint
$2 = 0x6e
(gdb) c
Continuing.
[0/1] Test
Thread 1 "test" hit Breakpoint 1, lib.deunicodeCustomAlloc (allocator=..., s=..., custom_placeholder=...) at lib.zig:122
122 const res = if (has_next_cache) blk: {
(gdb) p/x *(int*)&codepoint
$3 = 0x65
test "test conversion alloc" {
try testing.expect(try checkConversionAlloc("Æneid", "AEneid"));
}
>>> chr(0xc6)
'Æ'
>>> chr(0x6e)
'n'
>>> chr(0x65)
'e'
那么问题就是出在我们的 getReplacement 的实现上了。一种可能是我们的 getReplacement 被莫名奇妙 inline 掉了,一些 buffer 上的操作导致污染了栈空间的变量。通过执行 layout split 我们可以看到汇编代码
0x1035dea <lib.deunicodeCustomAlloc+3306> call 0x103acc0 <lib.getReplacement>
这里使用了 call ,所以并没有被 inline 。当然在非 inline 的情况下,也是可以污染栈空间的。缓冲区溢出攻击和是否 inline 其实也没啥关系的。但是这一行提醒了我
0x1035dc8 <lib.deunicodeCustomAlloc+3272> call 0x103ac70 <debug.FullPanic((function 'defaultPanic')).unwrapNull>
现代语言的默认编译 flag 下,都是有越界 panic 保护的。我们的代码没有 panic,所以非常大的概率我们的栈空间没有被污染掉!我们需要调整 debug 的方向
我们直接 break lib.getReplacement 来看一下这个函数被编译成什么样子,以下是失败版本的
(gdb) break lib.getReplacement
Breakpoint 1 at 0x103ac94
(gdb) r
Starting program: /home/kumiko/deunicode/.zig-cache/o/10c1e6a7582bccf5604417e043694e46/test
[New LWP 572209]
Thread 1 "test" hit Breakpoint 1, 0x000000000103ac94 in lib.getReplacement (cp=572209)
(gdb) info registers
rax 0xc6 198
rbx 0x1 1
rcx 0x0 0
rdx 0xc6 198
rsi 0x7fffffffbe00 140737488338432
rdi 0x7fffffffc378 140737488339832
rbp 0x7fffffffbf70 0x7fffffffbf70
rsp 0x7fffffffbf70 0x7fffffffbf70
r8 0x1 1
r9 0x7fffffffd1a8 140737488343464
r10 0x7fffffffa3d8 140737488331736
r11 0x0 0
r12 0x0 0
r13 0x0 0
r14 0x0 0
r15 0x0 0
rip 0x103ac94 0x103ac94 <lib.getReplacement+4>
eflags 0x202 [ IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
fs_base 0x11ec010 18792464
gs_base 0x0 0
(gdb) disassemble lib.getReplacement
Attempt to extract a component of a value that is not a structure.
(gdb) x/32i $pc
=> 0x103ac94 <lib.getReplacement+4>: sub $0x150,%rsp
0x103ac9b <lib.getReplacement+11>: mov %eax,0x130(%rsp)
0x103aca2 <lib.getReplacement+18>: mov %eax,%eax
0x103aca4 <lib.getReplacement+20>: mov %rax,(%rsp)
0x103aca8 <lib.getReplacement+24>: mov $0x11eb000,%rbx
0x103acaf <lib.getReplacement+31>: mov (%rbx),%rdx
0x103acb2 <lib.getReplacement+34>: lea 0x8(%rsp),%rbx
0x103acb7 <lib.getReplacement+39>: mov %rbx,0x18(%rsp)
0x103acbc <lib.getReplacement+44>: mov 0x18(%rsp),%rdi
0x103acc1 <lib.getReplacement+49>: mov %rdx,0x20(%rsp)
0x103acc6 <lib.getReplacement+54>: mov 0x20(%rsp),%rsi
0x103accb <lib.getReplacement+59>: movq $0x10,0x28(%rsp)
0x103acd4 <lib.getReplacement+68>: mov 0x28(%rsp),%rcx
0x103acd9 <lib.getReplacement+73>: rep movsb %ds:(%rsi),%es:(%rdi)
0x103acdb <lib.getReplacement+75>: mov %rax,%rdx
0x103acde <lib.getReplacement+78>: cmp 0x10(%rsp),%rdx
0x103ace3 <lib.getReplacement+83>: jb 0x103acfc <lib.getReplacement+108>
0x103ace9 <lib.getReplacement+89>: mov $0x11f6be0,%rbx
0x103acf0 <lib.getReplacement+96>: mov (%rbx),%rax
0x103acf3 <lib.getReplacement+99>: mov 0x8(%rbx),%rdx
0x103acf7 <lib.getReplacement+103>: jmp 0x103b0ca <lib.getReplacement+1082>
0x103acfc <lib.getReplacement+108>: jmp 0x103ad01 <lib.getReplacement+113>
0x103ad01 <lib.getReplacement+113>: mov $0x11eb000,%rbx
0x103ad08 <lib.getReplacement+120>: mov (%rbx),%rdx
0x103ad0b <lib.getReplacement+123>: lea 0x30(%rsp),%rbx
0x103ad10 <lib.getReplacement+128>: mov %rbx,0x40(%rsp)
0x103ad15 <lib.getReplacement+133>: mov 0x40(%rsp),%rdi
0x103ad1a <lib.getReplacement+138>: mov %rdx,0x48(%rsp)
0x103ad1f <lib.getReplacement+143>: mov 0x48(%rsp),%rsi
0x103ad24 <lib.getReplacement+148>: movq $0x10,0x50(%rsp)
0x103ad2d <lib.getReplacement+157>: mov 0x50(%rsp),%rcx
0x103ad32 <lib.getReplacement+162>: rep movsb %ds:(%rsi),%es:(%rdi)
这里无法直接 disassemble lib.getReplacement 来查看汇编代码,但是可以通过 PC 寄存器来看。我们需要对比 getReplacement 的实现
pub fn getReplacement(cp: u21) ?[]const u8 {
const i = @as(usize, cp);
if (i >= POINTERS.*.len) {
return null;
}
const p = POINTERS.*[i];
// if length is 1 or 2, then the "pointer" data is used to store the char
const chars = if (p.len <= 2) blk: {
// NOTE: not p.chr
// break :blk POINTERS.*[i].chr[0..@as(usize, p.len)];
break :blk POINTERS.*[i].chr[0..@as(usize, p.len)];
} else blk: {
const map_pos = @as(usize, (@as(u16, p.chr[0]) | (@as(u16, p.chr[1]) << 8)));
// unknown characters are intentionally mapped to out of range length
const start = map_pos;
const end = @as(usize, map_pos + p.len);
if (start >= MAPPING.len or end >= MAPPING.len) {
return null;
}
break :blk MAPPING.*[start..end];
};
return chars;
}
源代码中一共有两个地方显式返回了 null,我们先来看第一处
if (i >= POINTERS.*.len) {
return null;
}
这个地方很有可能就是
0x103acde <lib.getReplacement+78>: cmp 0x10(%rsp),%rdx
0x103ace3 <lib.getReplacement+83>: jb 0x103acfc <lib.getReplacement+108>
直接在指令的地方下断点
(gdb) break *0x103acde
Breakpoint 2 at 0x103acde
(gdb) c
Continuing.
[0/1] Test
Thread 1 "test" hit Breakpoint 2, 0x000000000103acde in lib.getReplacement (cp=198)
(gdb) print/x $rdx
$1 = 0xc6
(gdb) x/gx $rsp + 0x10
0x7fffffffbe30: 0x0000000000000000
这里的 rdx 就是我们的 i 变量,而 0x10(%rsp) 则是我们的 POINTERS.*.len。对的它的值是 0 。这就造成了我们的 if 条件恒成立,然后返回了 null。OK,我们大致定位到问题所在的具体片段了
我们看测试通过的二进制版本,这里的值是啥
Starting program: /home/kumiko/deunicode/.zig-cache/o/4956a4125b6989c9d0d9954375054576/test
This GDB supports auto-downloading debuginfo from the following URLs:
<https://debuginfod.archlinux.org>
Enable debuginfod for this session? (y or [n]) y
Debuginfod has been enabled.
To make this setting permanent, add 'set debuginfod enabled on' to .gdbinit.
[New LWP 1148235]
Thread 1 "test" hit Breakpoint 1, 0x000000000103acc4 in lib.getReplacement (cp=1148235)
(gdb) x/32i $pc
=> 0x103acc4 <lib.getReplacement+4>: sub $0x150,%rsp
0x103accb <lib.getReplacement+11>: mov %eax,0x130(%rsp)
0x103acd2 <lib.getReplacement+18>: mov %eax,%eax
0x103acd4 <lib.getReplacement+20>: mov %rax,(%rsp)
0x103acd8 <lib.getReplacement+24>: mov $0x11eb000,%rbx
0x103acdf <lib.getReplacement+31>: mov (%rbx),%rdx
0x103ace2 <lib.getReplacement+34>: lea 0x8(%rsp),%rbx
0x103ace7 <lib.getReplacement+39>: mov %rbx,0x18(%rsp)
0x103acec <lib.getReplacement+44>: mov 0x18(%rsp),%rdi
0x103acf1 <lib.getReplacement+49>: mov %rdx,0x20(%rsp)
0x103acf6 <lib.getReplacement+54>: mov 0x20(%rsp),%rsi
0x103acfb <lib.getReplacement+59>: movq $0x10,0x28(%rsp)
0x103ad04 <lib.getReplacement+68>: mov 0x28(%rsp),%rcx
0x103ad09 <lib.getReplacement+73>: rep movsb %ds:(%rsi),%es:(%rdi)
0x103ad0b <lib.getReplacement+75>: mov %rax,%rdx
0x103ad0e <lib.getReplacement+78>: cmp 0x10(%rsp),%rdx
0x103ad13 <lib.getReplacement+83>: jb 0x103ad2c <lib.getReplacement+108>
0x103ad19 <lib.getReplacement+89>: mov $0x11f6be0,%rbx
0x103ad20 <lib.getReplacement+96>: mov (%rbx),%rax
0x103ad23 <lib.getReplacement+99>: mov 0x8(%rbx),%rdx
0x103ad27 <lib.getReplacement+103>: jmp 0x103b0fa <lib.getReplacement+1082>
0x103ad2c <lib.getReplacement+108>: jmp 0x103ad31 <lib.getReplacement+113>
--Type <RET> for more, q to quit, c to continue without paging--
0x103ad31 <lib.getReplacement+113>: mov $0x11eb000,%rbx
0x103ad38 <lib.getReplacement+120>: mov (%rbx),%rdx
0x103ad3b <lib.getReplacement+123>: lea 0x30(%rsp),%rbx
0x103ad40 <lib.getReplacement+128>: mov %rbx,0x40(%rsp)
0x103ad45 <lib.getReplacement+133>: mov 0x40(%rsp),%rdi
0x103ad4a <lib.getReplacement+138>: mov %rdx,0x48(%rsp)
0x103ad4f <lib.getReplacement+143>: mov 0x48(%rsp),%rsi
0x103ad54 <lib.getReplacement+148>: movq $0x10,0x50(%rsp)
0x103ad5d <lib.getReplacement+157>: mov 0x50(%rsp),%rcx
0x103ad62 <lib.getReplacement+162>: rep movsb %ds:(%rsi),%es:(%rdi)
(gdb) break *0x103ad0e
Breakpoint 2 at 0x103ad0e
(gdb) c
Continuing.
[0/1] Test
Thread 1 "test" hit Breakpoint 2, 0x000000000103ad0e in lib.getReplacement (cp=198)
(gdb) print/x $rdx
$1 = 0xc6
(gdb) x/gx $rsp + 0x10
0x7fffffffbb70: 0x0000000700000000
OK,这里是不会触发 if 条件返回 null 的。但是等等,这里的 0x0000000700000000 是什么?
回顾一下我们的结构体的定义
const Ptr = struct {
chr: [2]u8 align(1),
len: u8 align(1),
};
那么每个 Ptr 的大小是:
chr: [2]u8: 2 bytelen: u8: 1 byte
总共 3 个字节。对的,这里没有 4 字节 align 的,会造成寻址时候额外的负担。
const RAW_POINTERS = @embedFile("./pointers.bin");
const MAPPING = @embedFile("./mapping.txt");
var POINTERS: *[]const Ptr = @ptrCast(@alignCast(@constCast(&RAW_POINTERS)));
我们的 pointers.bin 文件是 420000 字节,420000 / 3 等于 140000。这里我们应该期望 POINTERS.*.len 的值为 140000 才对。所以说其实不管测试能通过还是不能通过的版本,我们的代码都是存在问题的,只是恰好它通过了而已!
我们针对这三行代码仔细研究一下
(gdb) info variables RAW_POINTERS
All variables matching regular expression "RAW_POINTERS":
File lib.zig:
18: static [420000:0]u8 * const lib.RAW_POINTERS;
(gdb) info address lib.RAW_POINTERS
Symbol "lib.RAW_POINTERS" is static storage at address 0x12a0720.
(gdb) info files
Symbols from "/home/kumiko/deunicode/.zig-cache/o/10c1e6a7582bccf5604417e043694e46/test".
Local exec file:
`/home/kumiko/deunicode/.zig-cache/o/10c1e6a7582bccf5604417e043694e46/test', file type elf64-x86-64.
Entry point: 0x116dd20
0x0000000001001000 - 0x0000000001004740 is .rodata
0x0000000001004740 - 0x0000000001004f40 is .rodata.cst
0x0000000001004f40 - 0x0000000001004f47 is .rodata.str
0x0000000001004f48 - 0x00000000010340a0 is .eh_frame
0x00000000010340a0 - 0x0000000001034f64 is .eh_frame_hdr
0x0000000001035000 - 0x00000000011e9d2a is .text
0x00000000011ea000 - 0x00000000011ea008 is .tdata
0x00000000011ea008 - 0x00000000011ea010 is .tbss
0x00000000011eb000 - 0x00000000011f6998 is .data
0x00000000011f69a0 - 0x00000000012a5c98 is .data.rel.ro
0x00000000012a5c98 - 0x00000000012a5cb0 is .got.plt
0x00000000012a5cc0 - 0x00000000012a6d40 is .bss
0x00007ffff7ffd120 - 0x00007ffff7ffd170 is .hash in system-supplied DSO at 0x7ffff7ffd000
0x00007ffff7ffd170 - 0x00007ffff7ffd1d4 is .gnu.hash in system-supplied DSO at 0x7ffff7ffd000
0x00007ffff7ffd1d8 - 0x00007ffff7ffd340 is .dynsym in system-supplied DSO at 0x7ffff7ffd000
0x00007ffff7ffd340 - 0x00007ffff7ffd3dc is .dynstr in system-supplied DSO at 0x7ffff7ffd000
0x00007ffff7ffd3dc - 0x00007ffff7ffd3fa is .gnu.version in system-supplied DSO at 0x7ffff7ffd000
0x00007ffff7ffd400 - 0x00007ffff7ffd438 is .gnu.version_d in system-supplied DSO at 0x7ffff7ffd000
0x00007ffff7ffd438 - 0x00007ffff7ffd558 is .dynamic in system-supplied DSO at 0x7ffff7ffd000
0x00007ffff7ffd560 - 0x00007ffff7ffd570 is .rodata in system-supplied DSO at 0x7ffff7ffd000
0x00007ffff7ffd570 - 0x00007ffff7ffd5c4 is .note in system-supplied DSO at 0x7ffff7ffd000
0x00007ffff7ffd5c4 - 0x00007ffff7ffd618 is .eh_frame_hdr in system-supplied DSO at 0x7ffff7ffd000
0x00007ffff7ffd618 - 0x00007ffff7ffd778 is .eh_frame in system-supplied DSO at 0x7ffff7ffd000
0x00007ffff7ffd780 - 0x00007ffff7ffe6ed is .text in system-supplied DSO at 0x7ffff7ffd000
0x00007ffff7ffe6ed - 0x00007ffff7ffe7bf is .altinstructions in system-supplied DSO at 0x7ffff7ffd000
0x00007ffff7ffe7bf - 0x00007ffff7ffe7fb is .altinstr_replacement in system-supplied DSO at 0x7ffff7ffd000
RAW_POINTERS 的地址为 0x12a0720
.rodata的范围是0x1001000 - 0x1004740.data.rel.ro的范围是0x11f69a0 - 0x12a5c98
因为我们使用的 var 而不是 const 来定义的变量,所以 RAW_POINTERS 位于 .data.rel.ro 中。下面是两个 section 的差别,摘录自 GPT:
.rodata.rodata(read-only data)节用于存放编译期就能确定值、而且在程序运行期间永远不需要修改的“只读数据”。典型内容包括:字符串常量(如"hello world")、const修饰的全局/静态变量(其值是 compile-time constant)等。 ps-hung+2Kayssel+2这部分数据在加载到内存后通常被映射成只读 (read-only) —— 即程序不能写入它,从而保证常量不会被意外修改。 LWN.net+1
.data.rel.ro.data.rel.ro节(“data, relocation-relro”)用于那些“如果只是常量,就应当是只读,但因为它们需要重定位 (relocation) —— 即它们的最终地址 / 值 在链接时或运行时才确定” 的数据。 Stack Overflow+2MaskRay+2换句话说,如果一个全局/静态变量是“常量语义 (程序中不再修改)”但它初始化所依赖的内容(例如函数指针,或其他变量/符号地址)在编译时未知,需要通过重定位 (relocation) 来修正其地址,这种数据就放到
.data.rel.ro。 Stack Overflow+2The Rust Programming Language Forum+2在动态链接 (shared libraries / PIE) 的情况下,链接器和运行时 linker 会先处理 relocation(把正确的地址写入
.data.rel.ro),然后把这个节设为只读,这就是所谓的 “RELRO” — 即 “Relocation Read-Only” 机制。 Stack Overflow+2
稍微绕远了,让我们回到问题本身上,下面这行代码为啥有问题。看起来很多 @ 就像工作群里面的消息一样令人烦躁
var POINTERS: *[]const Ptr = @ptrCast(@alignCast(@constCast(&RAW_POINTERS)));
为什么我们通过单元测试的 binary,仅仅只有 len 不对呢?我们代码后面可是有使用
const p = POINTERS.*[i];
访问过 pointers.bin 中的数据的。我们知道 POINTERS 的类型是 *[]const Ptr,它是一个指针,指向了 slice。而 slice 的内存布局类似是这样的
struct {
void* ptr;
usize len;
}
我们再使用 GDB 来看一下
- 测试通过的
(gdb) info address lib.POINTERS
Symbol "lib.POINTERS" is static storage at address 0x11eb000.
(gdb) x/gx 0x11eb000
0x11eb000 <lib.POINTERS>: 0x00000000012a0720
(gdb) x/2gx 0x12a0720
0x12a0720 <lib.RAW_POINTERS>: 0x0000000001217b91 0x0000000700000000
- 不通过的
(gdb) info variables lib.POINTERS
All variables matching regular expression "lib.POINTERS":
File lib.zig:
20: static struct []const lib.Ptr *lib.POINTERS;
(gdb) info address lib.POINTERS
Symbol "lib.POINTERS" is static storage at address 0x11eb000.
(gdb) x/gx 0x11eb000
0x11eb000 <lib.POINTERS>: 0x00000000012a0720
(gdb) x/2gx 0x12a0720
0x12a0720 <lib.RAW_POINTERS>: 0x0000000001217b91 0x0000000000000000
0x0000000001217b91 这个地址是对的,就是存储的我们 pointers.bin 中的二进制内容(图有字节序问题,不妨碍)

我们来整理一下当前的状况:
RAW_POINTERS是一个指针,指向了嵌入到二进制中的pointers.bin的数据起始位置POINTERS是一个指针,指向了 slice header- slice header 是一个内存区域,它的
ptr部分指向了RAW_POINTERS变量的地址。而len是错的
大概就是这样子
POINTERS(0x11eb000) -> slice header(0x00000000012a0720) == RAW_POINTERS(0x00000000012a0720)
|
- *ptr 0x0000000001217b91 -> pointers.bin 文件
- len 0x0000000000000000
我们 RAW_POINTERS 和 slice header 是同一个内存区域,那么为什么使用 RAW_POINTERS.len 就不会有问题呢?莫非其实它的 len 存储在其他的地方?我们来看一下文档
@embedFile(comptime path: []const u8) *const [N:0]u8
This function returns a compile time constant pointer to null-terminated, fixed-size array with length equal to the byte count of the file given by
path. The contents of the array are the contents of the file. This is equivalent to a string literal with the file contents.
embedFile 返回的是 *const [N:0]u8 类型。它是指向长度为 N 的数组的指针。
Zig 的数组类型 [N:sentinel]T 表示:
- 一个长度为
N的固定长度数组 - 数组最后自动带一个 sentinel(这里是
0) - 用来表示 C 风格的字符串等
它并不是一个 slice header 的结构!长度 N 是直接被编译进去的常量。所以我们在转换成 slice header 的时候 len 的区域是一个随机值的内存。我们修改代码无法复现 bug 是因为我们的 .text section 的长度变了,导致我们 .data.rel.ro 会被重新计算,这就导致 len 的值是不一样的
所以也很好修,只要将 len 的值覆盖一次就可以避免了
