无C标准库依赖时ARM64汇编如何实现堆内存分配
ARMv8-A 汇编无C标准库依赖的堆内存分配实现方法
首先纠正对brk()系统调用的认知偏差
brk()本身完全支持内存释放,你之前的判断有误:
brk()的核心作用是修改进程堆段的结束边界(program break),只要你把这个边界向低地址移动,内核会自动回收边界以上的物理内存,这就是标准的内存释放操作。- 举个实际场景:初始堆边界为0x0000aaaabbbbb000,调用
brk()将边界设为0x0000aaaabbbbb000 + 16384,即申请了16KB堆内存;后续再调用brk()将边界改回初始值,这16KB内存就会被完整交还给内核。 - 常提到的
sbrk()是C库对brk()的封装,传入偏移量返回旧边界值,传入负偏移即可收缩堆,本质和直接调用brk()没有区别。
AArch64(ARMv8-A)下直接调用brk()的方法
AArch64架构Linux系统调用遵循如下约定:系统调用号存入x8寄存器,参数按顺序存入x0-x5寄存器,执行svc #0指令触发调用,返回值存在x0寄存器。
brk()的系统调用号为214:x0传入目标堆边界地址,调用成功后x0返回实际生效的堆边界,若返回值小于传入的目标地址则代表调用失败。- 首次调用
brk()时可给x0传0,即可获取进程初始的堆边界地址,你可以自行维护全局变量存储堆基址、当前堆边界、空闲块链表等管理数据。
基础调用示例:
// 获取初始堆边界 mov x8, #214 // 加载brk系统调用号 mov x0, #0 // 传0查询当前堆边界 svc #0 mov x19, x0 // x19存堆基址(初始边界) mov x20, x0 // x20存当前堆边界 // 申请4KB内存 add x0, x20, #4096 // 计算新堆边界 = 当前边界 + 4KB mov x8, #214 svc #0 cmp x0, x20 // 校验返回值是否符合预期 blt alloc_fail // 返回值小于当前边界则分配失败 mov x0, x20 // 分配到的内存起始地址为旧堆边界 add x20, x20, #4096 // 更新当前堆边界 // 释放堆顶的4KB内存 mov x0, x19 // 将堆边界改回初始位置 mov x8, #214 svc #0 mov x20, x19 // 更新当前堆边界,释放完成
实现通用分配/释放逻辑需要补充用户态堆管理
你觉得brk()没法自由释放内存,本质是因为brk()只能直接收缩堆顶的内存,无法直接释放堆中间位置的内存——这不是brk()的缺陷,而是堆内存分配器的通用设计:所有内存的申请释放逻辑都需要在用户态实现,C库的malloc/free也只是在brk()、mmap()这些系统调用之上加了一层管理逻辑而已,你完全可以自己用汇编写这部分,不需要依赖C库:
- 给每个分配的内存块加固定长度的头部元数据,记录块大小、是否空闲、相邻块地址等信息
- 分配内存时优先遍历已有的空闲块,匹配大小合适的块拆分使用,没有合适的空闲块时再调用
brk()扩容堆段 - 释放内存时将对应块标记为空闲,立刻合并前后相邻的空闲块;如果合并后的空闲块位于堆的最顶端,就调用
brk()降低堆边界,把内存正式还给内核
大内存分配的补充方案
如果要分配大于128KB的内存块,不需要走brk()堆管理逻辑,可以直接调用mmap()申请匿名私有内存映射:
- AArch64下
mmap()系统调用号为222,munmap()系统调用号为215,参数语义和C库封装的同名函数完全一致 - 这类内存释放时直接调用
munmap()即可,不需要等待堆边界收缩,释放后立刻归还内核
提示:所有Linux系统调用都不需要依赖C库,只要按照架构约定的传参规则触发软中断即可直接使用,C库本身也是用相同方式调用这些系统调用实现上层功能的。
内容的提问来源于stack exchange,提问作者EclecticPasta
相关产品推荐
相关产品推荐

