malloc 分配大堆块(128KB)的一次探索

前言

一次意外执行了 malloc(0x5000),结构使用 gdb 调试发现其分配的位置在 TLS 区域,这令我不解(:最后去看了下 malloc 源码和 mmap 源码实现,发现似乎可能gdb 插件的问题,乐

场景复现

c 复制代码
#include <stdio.h>
int main() {
        malloc(0x5000);
        return 0;
}

调试:

wtf 不应该啊,这咋能分配到 tls 区域去了呢?

问题探索

通过对 malloc 源码的查看,发现其超过了 mmap_threshold,所有最后走的 mmap 映射的一段私有匿名区域(这里大家应该都很熟悉了)所以不应该分配到 tls 区域去啊,在与 henry 师傅交流后,觉得可能与 mmap 的行为有关,于是说干就干,简单地去审了一下 mmap 的源码,最后发现可能是插件的问题

mmap 源码分析就不写了,我也没做笔记,毕竟是开源的,需要时自己去看看就行了,而且我看完 mmap 源码时已经快凌晨 3 点了,就简单说下 mmap 是如何分配虚拟内存的。在内核态,为进程维护了一颗红黑树,其根据地址组织了空闲的 vma(struct vm_area_struct 结构体表示),而在我的 ubuntu 22.04 上,文件与匿名映射区是从高地址往低地址增长的,所以 mmap 在寻找虚拟内存区域时是从高地址往低地址扫描的

知道这之后一切都可以解释了,简单调试一下,啥也不执行,看下内存布局:把随机化关了,方便调试

可以看到在 tls 前后存在 0x17e0000x11000 的空间,我们来验证一下:

c 复制代码
#include <stdio.h>
#include <sys/mman.h>
int main() {
        void *p0 = malloc(0x17e000-0x1000);
        void *p1 = mmap(0, 0x11000, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
        printf("p0: %p\np1: %p\n", p0, p1);
        return 0;
}

我的机器上 mmap_threshold = 128kb,所以 malloc(0x11000-0x1000) 会走 brk,所以这里我直接使用 mmap

关于这里 malloc 分配为啥要减去 0x1000 请自行审计 sysmalloc 源码(大家应该比较熟悉)

调试结果如下:

可以看到 mmap0x11000 区域就是 tls 下方的区域,malloc0x17e000 就是 tls 前面的那块区域,这里的 gdb 插件把其当作 TLS 了似乎。当然这里可能就是 tls 预留的,我没有具体调试内核,仅仅静态分析了 mmap 的代码。当然我通过分配一些线程局部变量调整了一下 tls 区域的位置,然后也是这样的:

c 复制代码
#include <sys/mman.h>
__thread char p0[0x17e000+0x1000];
int main() {
        void *p1 = mmap(0, 0x193000, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
        printf("p0: %p\np1: %p\n", p0, p1);
        return 0;
}

调试结果:

所以当我们 malloc 超过 0x17e000-0x1000 时,就会在 libc 前面映射一段空间,也就是大家打 CTF 时说的 malloc 一块大堆块,会使用 mmaplibc 前面分配一个堆块,一般网上都是说分配 0x200000,其实自己简单算一算就知道了,验证一下:

c 复制代码
#include <stdio.h>
#include <sys/mman.h>
int main() {
        void *p0 = malloc(0x17e000);
        printf("p0: %p\n", p0);
        return 0;
}

调试结果如下:

一切都是吻合的

一个有趣的 demo

这里给大家看一个 demo,感兴趣的可以自行研究下背后的原因

c 复制代码
#include <stdio.h>
#include <stdint.h>
#include <string.h>

int show() {
    FILE *maps_file;
    char line[256];
    char pathname[256];

    maps_file = fopen("/proc/self/maps", "r");
    if (maps_file == NULL) {
        perror("Failed to open /proc/self/maps");
        return 1;
    }

    while (fgets(line, sizeof(line), maps_file)) {
        if (sscanf(line, "%*x-%*x %*s %*x %*x:%*x %*u %s", pathname) == 1) {
            if (strcmp(pathname, "[stack]") == 0 || strcmp(pathname, "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2") == 0) {
                printf("%s", line);
            }
        }
    }

    fclose(maps_file);
    return 0;
}

int main() {
        uint64_t addr;
        show();
        puts("==========================================");
        scanf("%llx", &addr);
        *(uint64_t*)addr = 0xdeadbeef;
        puts("==========================================");
        show();
        return 0;
}

输出如下:

总结

mmap 的源码还是比较复杂的,我只是看了我感兴趣的部分,有兴趣的同学可以自己去看看源码,本来想说搭建环境调调 mmap 的,最后还是算了,太累了,睡觉了(:

当然这个东西本身连技术都不算,之所以记录一下,也算是回答自己刚开始学习时的一些疑问,记得当时看网上说 malloc 要分配 0x20000 才能够保证分配的空间在 libc 前面,其实根本没有问过自己为什么?也没有想过 mmap 底层是如何进行虚拟内存分配的?

当然其实现在来解决这些问题也挺好的,毕竟有一些基础,如果最开始就去看 mmap 的源码,相信我早就被劝退了

相关推荐
hope_wisdom2 个月前
C++网络编程之零拷贝技术
网络·网络编程·1024程序员节·mmap·零拷贝技术·sendfile·splice
橘色的喵4 个月前
[QNX] C++编程: 外部硬件加速器与Linux-ARM共享内存中使用NOCACHE的必要性与优化策略
linux·c++·缓存·arm·cache·共享内存·mmap
ustbxyls8 个月前
Java NIO中的mmap
java·开发语言·nio·mmap
请不要叫我菜鸡9 个月前
零拷贝技术、常见实现方案、Kafka中的零拷贝技术的使用、Kafka为什么这么快
数据库·分布式·后端·kafka·nio·mmap
morris1311 年前
【linux】NIO中的FileChannel与mmap
linux·nio·mmap·filechannel·随机读写
恋喵大鲤鱼1 年前
Linux mmap 的作用是什么?
linux·mmap
linux大本营1 年前
Linux内核mmap内存映射详解及例子实现
linux·mmu·内存管理·mmap·内存映射
晨少爱学习1 年前
mmap底层驱动实现(remap_pfn_range函数)
linux·驱动·虚拟内存·mmap·内存映射·remap_pfn_range