glibc源码分析-_2.23

优先分析版本:2.23,2.27,2.31,2.35

glibc2.23

也是终于开始分析源码了

吓哭了

malloc.c

这么长的注释是在写故事吗

malloc_chunk

prev_size(offset 0x0)

如果前一个chunk 被free了,记录bk chunk的size

也就是说,如果前一个chunk正在使用(allocated),它的user data 可以覆盖下一个chunk的prev_size,存在 space overlap

size(offset 0x4/0x8)

记录的是当前chunk的大小(including ‘size’)

由于8/16字节对齐,size的低3位恒为0,这3位就被glibc作为储存状态标志位

fd & bk(0x10 & 0x18)

fd_nextsize & bk_nextsize (0x20 & 0x28)

给large bin用的(free)

底下还有画的图

这是一个正在使用的chunk的布局

free掉的

malloc_state

mutex_t mutex

互斥锁

int flags

标志位

主要记录当前arena是否有fastbin,用于判断是否需要执行malloc_consolidate

mfastbinptr fastbinsY[NFASTBINS]

fastbins数组,放单向链表头

mchunkptr bins[NBINS * 2 - 2]

普通bins(unsorted,small,large)的数组

由于每个bin都是由fd和bk组成的双向链表头,成对出现,所以要用*2-2,unsorted bin 的fd , bk指针就在这个数组内

64 位系统下,NBINS 通常定义为 128

索引位置 对应指针 逻辑身份
<font style="color:rgb(68, 71, 70);">bins[0]</font> <font style="color:rgb(68, 71, 70);">fd</font> Unsorted Bin 的头指针(Forward)
<font style="color:rgb(68, 71, 70);">bins[1]</font> <font style="color:rgb(68, 71, 70);">bk</font> Unsorted Bin 的尾指针(Backward)
<font style="color:rgb(68, 71, 70);">bins[2]</font> <font style="color:rgb(68, 71, 70);">fd</font> Small Bin 2 的头指针
<font style="color:rgb(68, 71, 70);">bins[3]</font> <font style="color:rgb(68, 71, 70);">bk</font> Small Bin 2 的尾指针

mchunkptr top

指向top chunk的指针,在古老的houst of force有用

mchunkptr last_remainder

指向最近一次从 Unsorted Bin 拆分后剩下的那个“碎块”

unsigned int binmap [BINMAPSIZE]

malloc 不会逐个查bins是否为空,先看位图,如果某一位为0则对应bin为空

struct malloc_state *next

指向下一个arena

system_mem/max_system_mem

当前 arena 从系统申请的内存总量和历史峰值

static struct malloc_state main_arena

这是干什么的

main_arena是唯一静态定义的arena其他线程的arena是动态申请的

.attached_threads = 1

用于记录当前有多少个线程连接到这个arena

_int_malloc

真是好大一坨

checked_request2size (bytes, nb)

将请求的bytes转换为nb(normalized request size 16字节对齐)

av(arena) == NULL时,heap未初始化或其他原因,

直接调用syamalloc 申请 memory(brk or mmap)

下面怎么if else窜起来了

to fastbin

检查nb是否在fastbin范围内后用fastbin_index()将nb转化成idx,再用fastbin定位到该arena中的链表头指针

mchunkptr pp = *fb;

先尝试从单链表头取出一个chunk -> victim

victim == NULL则break

catomic_compare_and_exchange_val_acq()用于处理多线程

检查当前的fd是否等于victim,防止被其他线程抢先

用victim里的size重新算一遍idx,算出的idx必须等于进入if分支的idx

那么改fd的时候就要找附近的一个看起来是合法size的字节

to smallbin

可以看出smallbin用的是双向链表

判断大小,算idx,拿bin头指针

3410 if ((victim = last (bin)) != bin)

last(bin)就是bin->bk,如果bin->bk == bin,则链表为空,跳过

如果victim == 0,调用malloc_consolidate(),将初始化分配区

并合并fastbins中的所有的空闲块进入unsorted bin

检查victim->bk->fd = victim

3422 set_inuse_bit_at_offset (victim, nb);

脱链

标志位与指针转化

to largebin

malloc size 在largebin的区间时

to unsorted bin

到了unsorted bin画风就诡异起来了

可以发现,unsorted bin这一块都镶入了for( ; ; )死循环

不太好切片分析,我copy了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385

/*
Process recently freed or remaindered chunks, taking one only if
it is exact fit, or, if this a small request, the chunk is remainder from
the most recent non-exact fit. Place other traversed chunks in
bins. Note that this step is the only place in any routine where
chunks are placed in bins.

The outer loop here is needed because we might not realize until
near the end of malloc that we should have consolidated, so must
do so and retry. This happens at most once, and only when we would
otherwise need to expand memory to service a "small" request.
*/

for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
size = chunksize (victim);

/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/

if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{
/* split and reattach remainder */
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
av->last_remainder = remainder;
remainder->bk = remainder->fd = unsorted_chunks (av);
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}

set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

/* Take now instead of binning if exact fit */

if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/* place chunk in bin */

if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
{
fwd = bck;
bck = bck->bk;

victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert ((fwd->size & NON_MAIN_ARENA) == 0);
while ((unsigned long) size < fwd->size)
{
fwd = fwd->fd_nextsize;
assert ((fwd->size & NON_MAIN_ARENA) == 0);
}

if ((unsigned long) size == (unsigned long) fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

#define MAX_ITERS 10000
if (++iters >= MAX_ITERS)
break;
}

/*
If a large request, scan through the chunks of current bin in
sorted order to find smallest that fits. Use the skip list for this.
*/

if (!in_smallbin_range (nb))
{
bin = bin_at (av, idx);

/* skip scan if empty or largest chunk is too small */
if ((victim = first (bin)) != bin &&
(unsigned long) (victim->size) >= (unsigned long) (nb))
{
victim = victim->bk_nextsize;
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb)))
victim = victim->bk_nextsize;

/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin) && victim->size == victim->fd->size)
victim = victim->fd;

remainder_size = size - nb;
unlink (av, victim, bck, fwd);

/* Exhaust */
if (remainder_size < MINSIZE)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
}
/* Split */
else
{
remainder = chunk_at_offset (victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}

set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);
}
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

/*
Search for a chunk by scanning bins, starting with next largest
bin. This search is strictly by best-fit; i.e., the smallest
(with ties going to approximately the least recently used) chunk
that fits is selected.

The bitmap avoids needing to check that most blocks are nonempty.
The particular case of skipping all bins during warm-up phases
when no chunks have been returned yet is faster than it might look.
*/

++idx;
bin = bin_at (av, idx);
block = idx2block (idx);
map = av->binmap[block];
bit = idx2bit (idx);

for (;; )
{
/* Skip rest of block if there are no more set bits in this block. */
if (bit > map || bit == 0)
{
do
{
if (++block >= BINMAPSIZE) /* out of bins */
goto use_top;
}
while ((map = av->binmap[block]) == 0);

bin = bin_at (av, (block << BINMAPSHIFT));
bit = 1;
}

/* Advance to bin with set bit. There must be one. */
while ((bit & map) == 0)
{
bin = next_bin (bin);
bit <<= 1;
assert (bit != 0);
}

/* Inspect the bin. It is likely to be non-empty */
victim = last (bin);

/* If a false alarm (empty bin), clear the bit. */
if (victim == bin)
{
av->binmap[block] = map &= ~bit; /* Write through */
bin = next_bin (bin);
bit <<= 1;
}

else
{
size = chunksize (victim);

/* We know the first chunk in this bin is big enough to use. */
assert ((unsigned long) (size) >= (unsigned long) (nb));

remainder_size = size - nb;

/* unlink */
unlink (av, victim, bck, fwd);

/* Exhaust */
if (remainder_size < MINSIZE)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
}

/* Split */
else
{
remainder = chunk_at_offset (victim, nb);

/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks 2";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;

/* advertise as last remainder */
if (in_smallbin_range (nb))
av->last_remainder = remainder;
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);
}
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

use_top:
/*
If large enough, split off the chunk bordering the end of memory
(held in av->top). Note that this is in accord with the best-fit
search rule. In effect, av->top is treated as larger (and thus
less well fitting) than any other available chunk since it can
be extended to be as large as necessary (up to system
limitations).

We require that av->top always exists (i.e., has size >=
MINSIZE) after initialization, so if it would otherwise be
exhausted by current request, it is replenished. (The main
reason for ensuring it exists is that we may need MINSIZE space
to put in fenceposts in sysmalloc.)
*/

victim = av->top;
size = chunksize (victim);

if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
av->top = remainder;
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/* When we are using atomic ops to free fast chunks we can get
here for all block sizes. */
else if (have_fastchunks (av))
{
malloc_consolidate (av);
/* restore original bin index */
if (in_smallbin_range (nb))
idx = smallbin_index (nb);
else
idx = largebin_index (nb);
}

/*
Otherwise, relay to handle system-dependent cases
*/
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}
}
}


这就是unsorted bin的分拣规则吗

1.从 Unsorted Bin 摘下 chunk 并检查

首先,我们知道在glibc中bin是malloc_chunk结构体数组(in main_arena)

unsorted_chunks(av)为宏定义,指向main_arena中idx为1的chunk

这个chunk不在user数据区,它是当双向循环链表的“头”用的

这里的if有对chunk ->size的大小检查,

victim->size <= 2 * SIZE_SZ 是检查size最小值

victim->size > av->system_mem 检查最大值

size = chunksize (victim);用于提取victim->size,去掉低3位

2.尝试利用 last_remainder 快速切割 (针对 Small Request)

Last Remainder 机制

如果刚刚切开一个大块分给用户,剩下的那部分(Remainder)很有可能紧接着就被再次申请

可以看到if后面有好长一段判断逻辑

1
2
3
4
if (in_smallbin_range (nb) &&             // 1. 你申请的是个小块 (Small Bin 范围)
bck == unsorted_chunks (av) && // 2. Unsorted Bin 里【只有一个】chunk
victim == av->last_remainder && // 3. 这个 chunk 正好是“上次剩下的”
(unsigned long) (size) > (unsigned long) (nb + MINSIZE)) // 4. 它足够大

算余量,定新块,改链表

原来的去掉,新切的塞回来

收尾工作

check_malloced_chunk (av, victim, nb);

检查对齐

void *p = chunk2mem (victim);

p = (char *)victim + 2 * SIZE_SZ

alloc_perturb是什么

3.移出 Unsorted Bin 与精确匹配

将victim从双向循环链表中移除

没有safe unlinking(bck->fd == victim)

检查chunk大小,改PREV_INUSE,return p

4.分拣归仓,small bin or large bin

当 Unsorted Bin 中的 chunk 既没有被“精确匹配”拎走,也没有被“最后剩余块”逻辑切开时,分配器就会把它正式移出 Unsorted Bin,并根据它的 size 投递到Small BinLarge Bin

进smallbin

Small Bin 内部不排序,所有堆块大小相同,直接插入链表头部即可

进large bin就麻烦了

largebin 对比smallbin,它是有序的,按size从大到小

特性 Smallbin Largebin
尺寸 固定(0x20, 0x30…) 范围(Range)
排序 FIFO 按 Size 降序 + FIFO
指针数量 2 (<font style="color:rgb(68, 71, 70);">fd</font>, <font style="color:rgb(68, 71, 70);">bk</font>) 4 (<font style="color:rgb(68, 71, 70);">fd</font>, <font style="color:rgb(68, 71, 70);">bk</font>, <font style="color:rgb(68, 71, 70);">fd_ns</font>, <font style="color:rgb(68, 71, 70);">bk_ns</font>)
分配策略 精确匹配 最佳匹配 (Best-fit)
攻击难点 主要是 <font style="color:rgb(68, 71, 70);">unlink</font>
检查
需要操作 <font style="color:rgb(68, 71, 70);">nextsize</font>
指针,逻辑较深

为了加速查找,它引入了第二套指针:fd_nextsize 和 bk_nextsize(跳表结构)

情况1:当前堆块比 Bin 中最小的还要小

直接插入到链表尾部,并维护 nextsize 链表(循环结构)

情况2:寻找插入位置

情况3:大小已存在,插入到该大小对应组的第二个节点(优化性能,不更新 nextsize)

情况4:找到位置,插入新大小节点

情况5:Bin 原本为空,自己作为 nextsize 的起点


最终的链表链接

不论是 Small 还是 Large,最后都会执行这一组标准的双向链表插入操作

mark_bin()的作用

对于 Small Bin:新堆块总是插入到 Bin 头部,所以 bck 通常是 Bin 头部,fwd 是原本的第一块

对于 Large Binfwdbck 是根据大小排序逻辑计算出来的插入点

5. 扫描 Large Bin、Binmap 与 Top Chunk

如果 Unsorted Bin 遍历了一圈(默认最多 10000 次)还没返回,就只能去更大的 Bin 里找兜底方案了说是

防止极端情况下 unsorted 链表特别长导致这一轮 malloc 花费太久

也保护自己:如果链表结构被破坏(比如自环或环很大),可以避免在这里无限循环

5.1 Large Bin 的 Best-Fit 深度扫描

对“大请求”:从当前 largebin 中按 size 有序链找最佳 fit

接下来是这段(只有在“大请求”即 !in_smallbin_range(nb) 时走)只处理 large request

先检查这个 Large Bin 是否为空,或者里面最大的 chunk(链表头 first(bin))是否都比你要申请的 nb 小。如果是,直接跳过这个 bin

在 Large Bin 中,如果有多个大小完全相同的 chunk,它们会被挂在同一个位置,互相之间只通过 fdbk 链接,而 fd_nextsizebk_nextsize只保留在第一个(首个插入的)该大小的 chunk 上

拿走第二个 chunk,就不需要去nextsize指针

unlink

摘下 chunk 后,如果它比你申请的 nb 还要大,就需要进行切割

剩的太少,不切

够大就切

3635:算remainder指针

3638-3648:放回unsorted bin

清指针

重新打标签

return

5.2 Binmap 位图扫描


当前请求的 size 对应的 Bin 没货就++idx找下一个更大的

为了极速查找,glibc 把所有的 Bin 分成了几个大的 Block(每个 block 是一个整数,用 bit 代表对应的 bin 是否有货)

3693: 如果一个 block 是 0,说明这连续的几十个 Bin 全是空的,直接跳过整个 block

3691: 如果所有的 block 都扫完了还是没货,就会直接去找 Top Chunk

检查后unlink,剩下的太小就不切

大块就切

每次发生大块切割时,glibc 会检查你当前申请的尺寸 nb 是不是属于 Small Bin 的范围(通常是小于 0x400 字节)。如果是,它就会把切剩下的这块肉(remainder)的地址,记录在当前 arena 的 last_remainder 变量中

进large bin就清指针

5.3 use_top

前面的都不行就找top chunk(wilderness)


house of force

这里对 top chunk size 没有检查

没什么好说的,跳到3811

相当于消除内存碎片

最后没招了就向os申请

_int_free

_int_free不多,200多行

各个bin要用到的指针

check pointer and size

chunksize(p)读取p->mchunk_size并屏蔽(A|M|P)

3861-3862:防size过大,检查p指针16字节对齐

3873:size至少是0x20bytes

3868:打印错误

3879:确保当前chunk在使用中

_int_free处理的几类chunk

1.fastbin chunk

2.nom chunk

3.mapped chunk

fastbin 路径

3880-3956

触发条件:size <= get_max_fast()

如果开启了这个宏,并且当前释放的 chunk 物理上紧挨着 top chunk,那么它不能进入 fastbin,而是会进入后面的逻辑直接和 top chunk 合并

通过 chunk_at_offset(p, size) 找到当前 chunk 在内存中物理相邻的下一个 chunk。检查这个 next chunk 的 size 字段

如果当前没锁(have_lock==0),它会临时加锁重查一次,避免并发导致误报(malloc.c:3901-3919)

3879-3919:

这段检查 next chunk 头是否离谱:

next->size <= 2*SIZE_SZ 或 chunksize(next) >= av->system_mem

命中就报:free(): invalid next size (fast)

填充用户区数据

set_fastchunks(av):标记 arena 里存在 fastbin 块

idx = fastbin_index(size) 找 bin 下标

fb = &fastbin(av, idx) 拿到该链表头

注释写的还挺详细的

3935-3938:检查 bin 的顶部是不是我们准备添加的 chunk。仅检查 old == p,即“你要插入的 chunk 已经在链表头”能拦截 A -> free(A) -> free(A),但fastbin dup(A,B,A)不行

3944-3948:防多线程

普通 chunk(非 mmapped,且不进 fastbin)

3956-4098

要动双向链表,加个lock

3967:算nextchunk

一大坨检查

不能释放 top chunk,next chunk 不能越界,PREV_INUSE 检查,next chunk size 合法性

3999:填充字节进user data

向后合并 (Backward Consolidation) (4001-4007)

向前合并 (Forward Consolidation) (4009-4059)

如果nextchunk也free了,两个合并,放入unsorted bin

如果nextchuck是top chunk,直接跟top chunk合并

大块释放的后续处理 (4060-4098)

4076:合并fastbin chunk

4078-4092:如果合并后top chunk过大,则用sbrk或非main arena的用trim返还mem给os

lock检查

mmap chunk释放(4099-4107)

直接用munmap_chunk()


glibc源码分析-_2.23
https://ghostshark-pro.github.io/2026/04/23/glibc源码分析-_2.23/
Author
shark
Posted
2026年4月23日
License