tcp_init_sock 与 map_start 完整流程详解
2026-06-29 · 阅读 1
概述
本文档详细解释 KernelPatch 中 tcp_init_sock 区域作为临时存储空间的完整操作流程,包括备份、覆盖、恢复的每个步骤,以及 PAC/SCS 指令的处理机制。
整体架构
为什么使用 tcp_init_sock?
问题:_paging_init 代码需要在内核虚拟地址空间执行
但此时内存管理还未初始化,无法分配虚拟内存
解决方案:借用 tcp_init_sock 函数的内存区域
- tcp_init_sock 在内核启动早期不会被调用
- 完成内存映射后立即恢复
- 网络初始化在恢复之后才进行
关键数据结构
// kernel/base/start.h:12-28
typedef struct {
setup_header_t header;
version_t kernel_version;
int64_t kallsyms_lookup_name_offset;
int64_t kernel_size;
int64_t start_offset;
int64_t extra_size;
uint64_t kernel_pa;
int64_t map_offset; // ← tcp_init_sock 的偏移
int64_t map_backup_len; // ← 备份的大小
uint8_t map_backup[MAP_MAX_SIZE]; // ← 备份的数据(2KB)
// ...
} start_preset_t;
阶段 1:地址计算与选择(tools/symbol.c)
选择 map_area
// todo
void select_map_area(kallsym_t *kallsym, char *image_buf, int32_t *map_start, int32_t *max_size)
{
int32_t addr = 0x200;
addr = get_symbol_offset_exit(kallsym, image_buf, "tcp_init_sock");
// 使用向下对齐确保函数开头(包括PAC验证指令)被包含在备份区域中
// 如果使用向上对齐,函数开头可能被跳过,导致PAC指令未备份而AUC指令被NOP,造成崩溃
*map_start = align_floor(addr, 16);
// 增加max_size以补偿向下对齐可能包含的前置字节
*max_size = 0x800 + 16;
//打印tcp_init_sock附件的16条指令
uint32_t *code = (uint32_t *)(image_buf + addr);
for (int i = 0; i < 16; i++) {
tools_logi(" [%02x]: %08x", i*4, code[i]);
}
tools_logi("\n");
}
关键点:
- 向下对齐(align_floor):确保函数开头的 PAC/SCS 指令被包含
- 增加 max_size:补偿向下对齐可能包含的前置字节
- 打印前 16 条指令:用于调试,查看 PAC/SCS 指令
为什么向下对齐?
函数实际地址:0xD73553
向下对齐到 16:0xD73550
↓
备份区域:[0xD73550, 0xD73550 + 0x810)
包含完整的函数开头(包括 PAC/SCS 指令)✅
如果向上对齐:
函数实际地址:0xD73553
向上对齐到 16:0xD73560
↓
备份区域:[0xD73560, 0xD73560 + 0x800)
可能跳过函数开头的指令 ❌
阶段 2:备份阶段(start_prepare)
代码位置
// backup map area
// memcpy(start_preset.map_backup, kernel_pa + setup_preset.map_offset, (uint64_t)_map_end - (uint64_t)_map_start)
adrp x13, _map_end
add x13, x13, :lo12:_map_end
adrp x14, _map_start
add x14, x14, :lo12:_map_start
sub x2, x13, x14
// start_preset.map_backup_len = (uint64_t)_map_end - (uint64_t)_map_start
str x2, [x11, #start_map_backup_len_offset]
add x0, x11, #start_map_backup_offset
add x1, x19, x20
bl memcpy8
详细步骤
步骤 1:计算备份大小
adrp x13, _map_end
add x13, x13, :lo12:_map_end // x13 = _map_end 的链接地址
adrp x14, _map_start
add x14, x14, :lo12:_map_start // x14 = _map_start 的链接地址
sub x2, x13, x14 // x2 = _map_end - _map_start (~2KB)
说明:
_map_start和_map_end是链接器符号,定义在kpimg.lds中- 它们标记了
_paging_init代码的起始和结束 - 备份大小通常为 2-3KB
步骤 2:保存备份大小
str x2, [x11, #start_map_backup_len_offset] // 保存到 start_preset.map_backup_len
存储位置:
start_preset.map_backup_len(start_preset_t结构体)- 用于后续恢复时确定恢复范围
步骤 3:执行备份
add x0, x11, #start_map_backup_offset // x0 = start_preset.map_backup (目标)
add x1, x19, x20 // x1 = kernel_pa + map_offset (源)
bl memcpy8 // 执行备份
内存布局:
物理内存(备份时):
[kernel_pa + map_offset]:
tcp_init_sock 的原始机器码
├─ 可能包含:PACIASP (0xD503233F) 或 str x30, [x18], #8 (0xF85F8A5E)
├─ 函数序言:STP X29, X30, [SP, #-16]!
├─ 函数体:BL 指令、数据访问等
└─ 函数结尾:AUTIASP (0xD503231F) 或 ldr x30, [x18, #-8]! (0xF85F8A1E)
备份到:
[start_preset.map_backup] (在 kpimg 数据段):
完整的 tcp_init_sock 机器码副本
关键点:
- ✅ 备份的是物理内存内容(
kernel_pa + map_offset) - ✅ 包含完整的 PAC/SCS 指令(如果存在)
- ✅ 备份发生在覆盖之前,确保数据完整
阶段 3:覆盖阶段(map_prepare)
代码位置
// relocate map
// memcpy(preset.map_offset + kernel_pa, (uint64_t)_map_start, (int64_t)(_map_end - (uint64_t)_map_start));
adrp x2, _map_end
add x2, x2, :lo12:_map_end
adrp x1, _map_start
add x1, x1, :lo12:_map_start
sub x2, x2, x1
add x0, x19, x14
bl memcpy8
详细步骤
步骤 1:计算 _map_start 和 _map_end 的位置
adrp x2, _map_end
add x2, x2, :lo12:_map_end // x2 = _map_end 的链接地址
adrp x1, _map_start
add x1, x1, :lo12:_map_start // x1 = _map_start 的链接地址
sub x2, x2, x1 // x2 = _map_end - _map_start (代码大小)
说明:
_map_start和_map_end是链接器符号- 它们标记了
_paging_init代码的边界 - 代码已经在编译时链接到 kpimg 中
步骤 2:执行覆盖
add x0, x19, x14 // x0 = kernel_pa + map_offset (目标)
bl memcpy8 // 复制 _paging_init 代码
内存布局(覆盖后):
物理内存(覆盖后):
[kernel_pa + map_offset]:
_paging_init 的机器码
├─ get_myva() 函数
├─ get_data() 函数
├─ mem_proc() 函数
├─ get_or_create_pte() 函数
└─ _paging_init() 主函数
关键点:
- ❌ tcp_init_sock 的原始代码被覆盖
- ✅ 备份已经保存在
start_preset.map_backup中 - ✅ 覆盖发生在备份之后,数据安全
阶段 4:执行阶段(_paging_init)
代码执行流程
T1: 内核调用 paging_init
↓
T2: paging_init[0] = B _paging_init (劫持指令)
↓
T3: 跳转到 _paging_init(位于 tcp_init_sock 位置)
↓
T4: _paging_init() 执行
├─ 分配新的物理内存
├─ 创建页表映射
├─ 复制 kpimg 到新位置(包括 map_backup)
├─ 恢复 paging_init 原始指令
├─ 调用原始 paging_init()
└─ 调用 start() 继续启动
关键操作:复制 kpimg
for (uint64_t i = start_va; i < start_va + all_size; i += 8) {
*(uint64_t *)i = 0;
}
for (uint64_t i = 0; i < data->start_img_size; i += 8) {
*(uint64_t *)(start_va + i) = *(uint64_t *)(old_start_va + i);
}
for (uint64_t i = 0; i < data->extra_size; i += 8) {
*(uint64_t *)(start_va + data->start_size + i) = *(uint64_t *)(old_start_va + data->start_img_size + i);
}
flush_icache_all();
说明:
old_start_va:原始 kpimg 的虚拟地址(物理地址 + linear_voffset)start_va:新的 kpimg 的虚拟地址(新物理地址 + kimage_voffset)- 复制包括
start_preset.map_backup(在start_preset_t结构体中)
阶段 5:恢复阶段(restore_map)
代码位置
static void restore_map()
{
uint64_t start = kernel_va + start_preset.map_offset;
uint64_t end = start + start_preset.map_backup_len;
log_boot("Restore: %llx, %llx\n", start, end);
for (uint64_t i = start; i < align_ceil(end, page_size); i += page_size) {
uint64_t *pte = pgtable_entry_kernel(i);
uint64_t orig = *pte;
*pte = (orig | PTE_DBM) & ~PTE_RDONLY;
flush_tlb_kernel_page(i);
for (uint64_t j = i; j >= start && j < end && j < i + page_size; j += 8) {
*(uint64_t *)j = *(uint64_t *)(start_preset.map_backup + (j - start));
}
*pte = orig;
flush_tlb_kernel_page(i);
}
flush_icache_all();
}
详细步骤
步骤 1:计算恢复范围
uint64_t start = kernel_va + start_preset.map_offset; // tcp_init_sock 的虚拟地址
uint64_t end = start + start_preset.map_backup_len; // 结束地址
说明:
kernel_va:内核的虚拟基地址(KASLR 后的)start_preset.map_offset:tcp_init_sock 的偏移(来自 setup 阶段)start_preset.map_backup_len:备份的大小(来自备份阶段)
步骤 2:逐页恢复
for (uint64_t i = start; i < align_ceil(end, page_size); i += page_size) {
// 1. 获取页表项
uint64_t *pte = pgtable_entry_kernel(i);
uint64_t orig = *pte;
// 2. 临时设置为可写
*pte = (orig | PTE_DBM) & ~PTE_RDONLY;
flush_tlb_kernel_page(i);
// 3. 恢复数据
for (uint64_t j = i; j >= start && j < end && j < i + page_size; j += 8) {
*(uint64_t *)j = *(uint64_t *)(start_preset.map_backup + (j - start));
}
// 4. 恢复原始权限
*pte = orig;
flush_tlb_kernel_page(i);
}
为什么逐页处理?
- tcp_init_sock 可能跨越多个页面
- 不同页面的权限可能不同(代码段通常是只读)
- 必须临时修改权限才能写入,然后恢复原始权限
循环条件分析:
for (uint64_t j = i; j >= start && j < end && j < i + page_size; j += 8)
问题:j >= start 可能导致跳过第一个页的部分内容
修复建议:
uint64_t j_start = (i > start) ? i : start; // max(i, start)
uint64_t j_end = (end < i + page_size) ? end : (i + page_size); // min(end, i + page_size)
for (uint64_t j = j_start; j < j_end; j += 8) {
*(uint64_t *)j = *(uint64_t *)(start_preset.map_backup + (j - start));
}
步骤 3:刷新指令缓存
flush_icache_all();
说明:
- ARM64 有独立的指令缓存和数据缓存
- 修改代码后必须刷新 I-cache
- 否则 CPU 可能执行旧的缓存指令
PAC/SCS 指令处理
问题:是否需要 NOP 化 PAC/SCS 指令?
答案:不需要,而且不应该!
原因分析
1. 备份的是运行时指令
编译时(kernel 镜像):
tcp_init_sock:
PACIASP (0xD503233F)
运行时(KASLR 后,alternatives 替换后):
tcp_init_sock:
str x30, [x18], #8 (0xF85F8A5E) ← 可能已被 SCS 替换
说明:
- 备份发生在运行时,备份的是实际的运行时指令
- 如果系统使用 SCS,
paciasp已经被替换为str x30, [x18], #8 - 恢复时应该恢复运行时的指令,而不是编译时的指令
2. NOP 化会导致验证失败
函数正常流程(使用 PAC):
PACIASP ; 签名 X30
STP X29, X30, [SP, #-16]!
// ... 函数体 ...
LDP X29, X30, [SP], #16
AUTIASP ; 验证签名
RET
如果 AUTIASP 被 NOP 化:
PACIASP ; 签名 X30
STP X29, X30, [SP, #-16]!
// ... 函数体 ...
LDP X29, X30, [SP], #16
NOP ; ❌ 验证被跳过
RET ; X30 仍带签名
; → PAC 验证失败
; → X30 被修改为非法地址
; → 崩溃!
3. 正确的处理方式
方案 1:保持原样(推荐)
// 备份时:直接备份,不做任何修改
memcpy(start_preset.map_backup,
kernel_pa + map_offset,
map_backup_len);
// 恢复时:直接恢复,不做任何修改
memcpy(kernel_va + map_offset,
start_preset.map_backup,
map_backup_len);
优点:
- ✅ 简单可靠
- ✅ 保持函数的原始保护机制
- ✅ 兼容 PAC 和 SCS
方案 2:检测并处理(复杂,不推荐)
// 检测 PAC/SCS 指令
bool is_pac_or_scs(uint32_t insn) {
// PAC
if (insn == 0xD503233F) return true; // PACIASP
if (insn == 0xD503231F) return true; // AUTIASP
// SCS
if (insn == 0xF85F8A5E) return true; // str x30, [x18], #8
if (insn == 0xF85F8A1E) return true; // ldr x30, [x18, #-8]!
return false;
}
// 备份时:记录但不修改
void backup_map_area() {
memcpy(backup, source, size);
// 不修改任何指令
}
// 恢复时:直接恢复
void restore_map_area() {
memcpy(dest, backup, size);
// 不修改任何指令
}
当前实现的问题
问题 1:setup1.S 中的错误逻辑
mov w3, #0x201F
movk w3, #0xD503, lsl#16
orr w1, w3, #0x100
mov w2, #0xFFFFFD1F
and w0, w12, w2
// if ((map_data.paging_init_backup & 0xFFFFFD1F) == 0xD503211F)
cmp w0, w1
b.ne .backup
// map_data.paging_init_backup = NOP
mov w12, w3
// uint32_t *p = (uint32_t *)paging_init_pa + 1;
add x11, x13, #4
.cmp_auti:
// while ((*p & 0xFFFFFD1F) != 0xD503211F) ++p;
ldr w0, [x11], #4
and w0, w0, w2
cmp w0, w1
b.ne .cmp_auti
// *p = NOP
stur w3, [x11, #-4]
问题:
- ❌ 这段代码是针对
paging_init的,不是tcp_init_sock - ❌ 检测逻辑错误(实际检测的是 AUTIASP,不是 BTI C)
- ❌ 会 NOP 化 AUTIASP,导致 PAC 验证失败
修复:
- 删除这段代码
- 直接备份原始指令
问题 2:tcp_init_sock 备份的正确性
当前实现(正确):
// start_prepare: 直接备份,不做修改
add x0, x11, #start_map_backup_offset
add x1, x19, x20 // kernel_pa + map_offset
bl memcpy8 // 直接备份
优点:
- ✅ 备份完整的原始代码
- ✅ 包含 PAC/SCS 指令(如果存在)
- ✅ 不做任何修改
问题 3:恢复时的循环条件
for (uint64_t j = i; j >= start && j < end && j < i + page_size; j += 8) {
*(uint64_t *)j = *(uint64_t *)(start_preset.map_backup + (j - start));
}
问题:
j >= start条件可能导致跳过第一个页的部分内容- 如果
i < start,第一次循环会被跳过
修复:
uint64_t j_start = (i < start) ? start : i;
uint64_t j_end = (end < i + page_size) ? end : (i + page_size);
for (uint64_t j = j_start; j < j_end; j += 8) {
*(uint64_t *)j = *(uint64_t *)(start_preset.map_backup + (j - start));
}
完整时序图
┌─────────────────────────────────────────────────────────┐
│ T1: setup() 阶段 - start_prepare() │
└─────────────────────────────────────────────────────────┘
操作:
✅ 备份 tcp_init_sock 到 start_preset.map_backup
✅ 备份大小:_map_end - _map_start (~2KB)
✅ 备份内容:完整的原始机器码(包括 PAC/SCS)
物理内存:
[kernel_pa + map_offset]: tcp_init_sock 原始代码 ✅
┌─────────────────────────────────────────────────────────┐
│ T2: setup() 阶段 - map_prepare() │
└─────────────────────────────────────────────────────────┘
操作:
✅ 覆盖 tcp_init_sock 为 _paging_init 代码
✅ 备份 paging_init[0] 到 map_data.paging_init_backup
✅ 写入 B _paging_init 到 paging_init[0]
物理内存:
[kernel_pa + map_offset]: _paging_init 代码 ❌ (覆盖)
[kernel_pa + paging_init_offset]: B _paging_init ❌ (劫持)
┌─────────────────────────────────────────────────────────┐
│ T3: 内核启动,到达 paging_init │
└─────────────────────────────────────────────────────────┘
执行:
paging_init() → B _paging_init → 跳转到 _paging_init ✅
┌─────────────────────────────────────────────────────────┐
│ T4: _paging_init() 执行 │
└─────────────────────────────────────────────────────────┘
操作:
✅ 分配新的物理内存
✅ 创建页表映射
✅ 复制 kpimg 到新位置(包括 map_backup)
✅ 恢复 paging_init[0] = map_data.paging_init_backup
✅ 调用原始 paging_init()
✅ 调用 start()
虚拟内存:
[kernel_va + map_offset]: _paging_init 代码 ❌ (仍被覆盖)
[kernel_va + paging_init_offset]: 原始指令 ✅ (已恢复)
┌─────────────────────────────────────────────────────────┐
│ T5: start() 阶段 - restore_map() │
└─────────────────────────────────────────────────────────┘
操作:
✅ 从 start_preset.map_backup 恢复 tcp_init_sock
✅ 逐页恢复,临时修改权限
✅ 刷新指令缓存
虚拟内存:
[kernel_va + map_offset]: tcp_init_sock 原始代码 ✅ (已恢复)
┌─────────────────────────────────────────────────────────┐
│ T6: 网络初始化 - 调用 tcp_init_sock │
└─────────────────────────────────────────────────────────┘
执行:
tcp_init_sock() 正常执行 ✅
- PAC/SCS 指令正常工作
- 函数功能完全正常
关键要点总结
1. 备份时机
- ✅ 在覆盖之前备份(start_prepare 在 map_prepare 之前)
- ✅ 备份完整的原始代码(包括 PAC/SCS 指令)
- ✅ 不做任何修改(直接 memcpy)
2. 恢复时机
- ✅ 在 start() 早期恢复(在任何网络初始化之前)
- ✅ 恢复完整的原始代码(不做任何修改)
- ✅ 刷新指令缓存(确保 CPU 看到新代码)
3. PAC/SCS 指令处理
- ✅ 不需要 NOP 化(保持原样)
- ✅ 备份运行时指令(可能已被 SCS 替换)
- ✅ 恢复运行时指令(保持一致性)
4. 地址对齐
- ✅ 向下对齐(align_floor)确保包含函数开头
- ✅ 增加备份大小(补偿对齐可能的前置字节)
- ✅ 修复恢复循环(确保不跳过任何字节)
修复建议
1. 修复恢复循环条件
// kernel/base/start.c:317-335(修复版)
static void restore_map()
{
uint64_t start = kernel_va + start_preset.map_offset;
uint64_t end = start + start_preset.map_backup_len;
log_boot("Restore: %llx, %llx\n", start, end);
for (uint64_t i = align_floor(start, page_size);
i < align_ceil(end, page_size);
i += page_size) {
uint64_t *pte = pgtable_entry_kernel(i);
uint64_t orig = *pte;
*pte = (orig | PTE_DBM) & ~PTE_RDONLY;
flush_tlb_kernel_page(i);
// 修复:确保不跳过任何字节
uint64_t j_start = (i < start) ? start : i;
uint64_t j_end = (end < i + page_size) ? end : (i + page_size);
for (uint64_t j = j_start; j < j_end; j += 8) {
*(uint64_t *)j = *(uint64_t *)(start_preset.map_backup + (j - start));
}
*pte = orig;
flush_tlb_kernel_page(i);
}
flush_icache_all();
}
2. 删除 setup1.S 中的错误逻辑
// kernel/base/setup1.S:221-249(修复版)
map_prepare:
// ... 前面的代码 ...
// ============ 备份和劫持 paging_init ============
// 计算 paging_init 物理地址
add x13, x15, x19 // x13 = paging_init_offset + kernel_pa
// 读取第一条指令
ldr w12, [x13] // w12 = paging_init[0]
// ✅ 直接备份,不做任何修改
str w12, [x9, #map_paging_init_backup_offset]
dsb ish
// ... 生成 B 指令并写入 ...
3. 添加验证机制
// 在 restore_map() 后添加验证
void verify_map_restore()
{
uint64_t start = kernel_va + start_preset.map_offset;
uint32_t *actual = (uint32_t *)start;
uint32_t *backup = (uint32_t *)start_preset.map_backup;
int mismatch = 0;
for (int i = 0; i < start_preset.map_backup_len / 4; i++) {
if (actual[i] != backup[i]) {
log_boot("Mismatch at offset %x: actual=%08x, backup=%08x\n",
i * 4, actual[i], backup[i]);
mismatch++;
}
}
if (mismatch) {
log_boot("❌ Restore failed: %d words mismatch\n", mismatch);
} else {
log_boot("✅ Restore verified: all words match\n");
}
}
总结
核心原则
- 备份时:直接备份,不做任何修改
- 恢复时:直接恢复,不做任何修改
- PAC/SCS:保持原样,不需要 NOP 化
- 对齐:向下对齐,确保包含函数开头
- 验证:恢复后验证,确保数据正确
关键点
- ✅
tcp_init_sock用作临时存储,必须在网络初始化前恢复 - ✅ 备份发生在覆盖之前,数据安全
- ✅ 恢复发生在 start() 早期,时机正确
- ✅ PAC/SCS 指令应该保持原样,不需要 NOP 化
- ✅ 恢复循环需要修复,确保不跳过任何字节
参考资料
文档版本:2.0
最后更新:2026-06-26
相关 KernelPatch 版本:0.13.2+
评论
- 还没有评论,来说点什么吧。