# Go 1.24 Swiss Tables 适配文档

## 为什么有两个 Swiss Tables 函数？

在实现过程中，我们创建了两个函数来处理不同的追踪格式：

### 1. `inject_header_swiss` - W3C Traceparent 格式

**用途**：注入 W3C 标准的 traceparent header

**关键特征**：
- **参数类型**：`struct span_context*` (W3C 格式的 span context)
- **Header Key**：`"traceparent"` (W3C 标准)
- **Header Value 长度**：`W3C_VAL_LENGTH` (通常比 CW 格式短)
- **字符串转换**：使用 `span_context_to_w3c_string()` 函数
- **内存分配**：使用 `write_target_data()` (通用内存分配)

**调用路径**：
```
inject_header() 
  → inject_header_swiss()  (Go 1.24+)
```

### 2. `cw_inject_header_swiss` - CW (Coroot) 内部格式

**用途**：注入项目内部的 CW trace header

**关键特征**：
- **参数类型**：`struct apm_span_context*` (CW/APM 格式的 span context)
- **Header Key**：`"cwtrace"` (项目内部格式)
- **Header Value 长度**：`CW_HEADER_VAL_LENGTH` (123 bytes，包含更多信息)
- **字符串转换**：使用 `span_context_to_cw_string()` 函数
- **内存分配**：使用 `cw_write_target_data()` (带 proc_info 的进程隔离分配)

**调用路径**：
```
cw_inject_header() 
  → cw_inject_header_swiss()  (Go 1.24+)
cw_inject_header2() 
  → cw_inject_header_swiss()  (Go 1.24+)
```

### 为什么需要两个函数？

1. **不同的追踪标准**：
   - W3C Traceparent：行业标准，用于跨系统追踪
   - CW Trace：项目内部格式，可能包含更多元数据

2. **不同的数据结构**：
   - `span_context` vs `apm_span_context` - 不同的字段布局
   - 需要不同的序列化函数

3. **不同的内存分配策略**：
   - `write_target_data()` - 通用分配
   - `cw_write_target_data()` - 进程隔离分配（带 proc_info）

### 代码对比

```c
// W3C 格式
inject_header_swiss():
  - key: "traceparent" (W3C_KEY_LENGTH)
  - value: span_context_to_w3c_string()
  - alloc: write_target_data()

// CW 格式  
cw_inject_header_swiss():
  - key: "cwtrace" (CW_HEADER_KEY_LENGTH)
  - value: span_context_to_cw_string()
  - alloc: cw_write_target_data(..., proc_info)
```

### 总结

两个函数实现了相同的 Swiss Tables 注入逻辑，但：
- 处理不同的追踪格式（W3C vs CW）
- 使用不同的数据结构（span_context vs apm_span_context）
- 使用不同的内存分配方法

这是为了保持向后兼容性，同时支持两种追踪格式。

---

# Go 1.24 Swiss Tables 适配文档

## 问题背景

Go 1.24 引入了新的 map 实现（Swiss Tables），替代了传统的 bucket-based hash table。这导致 `euspace` 项目在监控 Go 1.24 应用时出现不兼容问题。

### 问题发现

在测试过程中发现，Go 1.24 应用的监控功能无法正常工作，具体表现为：

```
2025-11-04 10:13:11.68|ERROR|3162137|tls.go:291] [AttachGoTlsUprobes] STEP 12.2: Failed to get buckets offset, pid=3152597, version=go1.24.0
```

**根本原因**：Go 1.24 使用 Swiss Tables 实现，`runtime.hmap` 结构体不再存在 `buckets` 字段。

## 解决方案

### 1. 源码分析

通过查看 Go 1.24 源码（`/root/code/go`），我们发现：

#### 旧实现（Go < 1.24）
- 使用 `runtime.hmap` 结构体
- 包含 `buckets unsafe.Pointer` 字段（指向 bucket 数组）
- 结构体位置：`src/runtime/map_noswiss.go`

```go
type hmap struct {
    count     int
    flags     uint8
    B         uint8
    noverflow uint16
    hash0     uint32
    buckets   unsafe.Pointer  // ← 旧的字段
    oldbuckets unsafe.Pointer
    // ...
}
```

#### 新实现（Go 1.24+）
- 使用 `internal/runtime/maps.Map` 结构体
- 包含 `dirPtr unsafe.Pointer` 字段（替代 `buckets`）
- 结构体位置：`src/internal/runtime/maps/map.go`

```go
type Map struct {
    used      uint64      // offset 0
    seed      uintptr     // offset 8
    dirPtr    unsafe.Pointer  // offset 16 ← 新的字段
    dirLen    int         // offset 24
    globalDepth uint8
    globalShift uint8
    writing   uint8
    clearSeq  uint64
}
```

### 2. 数据结构差异

#### 小 Map（dirLen == 0）
- `dirPtr` 直接指向一个 `group`
- `group` 结构：
  ```
  +------------------+
  | ctrlGroup (8B)   |  ← 控制字，每个字节表示一个 slot 的状态
  +------------------+
  | slot[0] (40B)    |  ← key (16B) + elem (24B)
  | slot[1] (40B)    |
  | ...              |
  | slot[7] (40B)    |
  +------------------+
  ```

#### 大 Map（dirLen > 0）
- `dirPtr` 指向 directory（table 指针数组）
- 需要访问 directory → table → group → slot

### 3. 实现适配

#### 3.1 获取偏移量（Go 层）

**文件**：`ebpftracer/tls.go`

**修改点**：
1. 尝试获取 `runtime.hmap.buckets`（向后兼容）
2. 如果失败，尝试获取 `internal/runtime/maps.Map.dirPtr`
3. 如果 DWARF 查找失败，使用硬编码偏移量 16（64-bit 系统）

```go
// 尝试获取 buckets 字段
bucketsOff, ok2 := tracer.GetOffset(tracer.NewID("std", "runtime", "hmap", "buckets"), path)

if !ok2 {
    // Go 1.24+ Swiss Tables
    // 尝试获取 maps.Map.dirPtr
    swissFields := []struct {
        pkg        string
        structName string
        field      string
    }{
        {"internal/runtime/maps", "Map", "dirPtr"},
        {"internal.runtime.maps", "Map", "dirPtr"},
        {"maps", "Map", "dirPtr"},
    }
    
    for _, sf := range swissFields {
        swissOff, swissOk := tracer.GetOffset(tracer.NewID("std", sf.pkg, sf.structName, sf.field), path)
        if swissOk {
            bucketsOff = swissOff
            ok2 = true
            break
        }
    }
    
    // 如果还是失败，使用硬编码（基于源码分析）
    if !ok2 && major >= 1 && minor >= 24 {
        bucketsOff = 16  // used(8) + seed(8) + dirPtr(16)
        ok2 = true
    }
}
```

**结果**：成功获取到 `dirPtr` 偏移量（16），并正确设置到 `proc_info->buckets_ptr_pos`。

#### 3.2 Header 注入实现（eBPF 层）

**文件**：`ebpftracer/ebpf/utrace/go/net/client.probe.bpf.c`

**新增函数**：
1. `inject_header_swiss()` - W3C traceparent 格式
2. `cw_inject_header_swiss()` - CW 格式

**实现逻辑**：

```c
static __always_inline long inject_header_swiss(...) {
    // 1. 读取 used 计数（maps.Map 的第一个字段）
    u64 used = 0;
    bpf_probe_read_user(&used, sizeof(used), headers_ptr);
    
    // 2. 检查 map 是否已满
    if (used >= 8) return -1;
    
    // 3. 读取 dirLen（offset 24）判断 map 类型
    s32 dirLen = 0;
    bpf_probe_read_user(&dirLen, sizeof(dirLen), headers_ptr + 24);
    
    // 4. 如果是小 map（dirLen == 0），读取 dirPtr
    void *group_ptr = NULL;
    if (dirLen == 0) {
        bpf_probe_read_user(&group_ptr, sizeof(group_ptr), 
                           headers_ptr + buckets_ptr_pos);
    } else {
        // 大 map 暂不支持
        return -1;
    }
    
    // 5. 读取 control word（group 的前 8 字节）
    u64 ctrls = 0;
    bpf_probe_read_user(&ctrls, sizeof(ctrls), group_ptr);
    
    // 6. 查找第一个空 slot（ctrl byte == 0x80）
    u8 slot_idx = 0;
    for (u8 i = 0; i < 8; i++) {
        u8 ctrl_byte = (ctrls >> (i * 8)) & 0xFF;
        if (ctrl_byte == 0x80) {
            slot_idx = i;
            break;
        }
    }
    
    // 7. 计算 slot 偏移量
    // group 布局：ctrlGroup(8) + slots[8]
    // 每个 slot：key(16) + elem(24) = 40 bytes
    u64 slot_offset = 8 + (slot_idx * 40);
    void *slot_ptr = group_ptr + slot_offset;
    
    // 8. 写入 key（go_string_ot）
    char key[W3C_KEY_LENGTH] = "traceparent";
    void *key_str_ptr = write_target_data(key, W3C_KEY_LENGTH);
    struct go_string_ot key_str = {.str = key_str_ptr, .len = W3C_KEY_LENGTH};
    bpf_probe_write_user(slot_ptr, &key_str, sizeof(key_str));
    
    // 9. 写入 value（go_slice_ot）
    char val[W3C_VAL_LENGTH];
    span_context_to_w3c_string(propagated_ctx, val);
    // ... 创建 go_string_ot 和 go_slice_ot
    void *elem_ptr = slot_ptr + 16;  // 在 key 之后
    bpf_probe_write_user(elem_ptr, &val_slice, sizeof(val_slice));
    
    // 10. 更新 control byte
    u8 new_ctrl = 0x5A;  // 占位符，应该是 H2 hash
    u64 ctrl_mask = 0xFFULL << (slot_idx * 8);
    ctrls = (ctrls & ~ctrl_mask) | (((u64)new_ctrl) << (slot_idx * 8));
    bpf_probe_write_user(group_ptr, &ctrls, sizeof(ctrls));
    
    // 11. 更新 used 计数
    used += 1;
    bpf_probe_write_user(headers_ptr, &used, sizeof(used));
    
    return 0;
}
```

#### 3.3 版本路由

**修改点**：所有 header 注入函数都添加了版本检测

```c
// 在 inject_header(), cw_inject_header(), cw_inject_header2() 中
if (proc_info->version >= GO_VERSION(1, 24, 0)) {
    return inject_header_swiss(...);  // 或 cw_inject_header_swiss(...)
}
// 否则使用旧的实现
```

#### 3.4 调试日志

所有关键步骤都添加了 `bpf_printk` 日志：

- 函数入口：`START`
- 读取的值：`used`, `dirLen`, `group_ptr`, `ctrls`
- 查找结果：`Found empty slot at index X`
- 计算的值：`slot_idx`, `slot_offset`, `slot_ptr`
- 内存分配：`key_str_ptr`, `val_str_ptr` 等
- 写入操作：每个步骤的成功/失败状态
- 更新操作：control word 和 used count 的更新
- 函数完成：`SUCCESS`

## 技术细节

### Control Byte 格式

Swiss Tables 使用 control byte 标记每个 slot 的状态：

- `0x80` (0b10000000) = 空 slot
- `0xFE` (0b11111110) = 已删除（tombstone）
- `0x00-0x7F` = 已使用，低 7 位是 H2 hash

### Slot 布局（map[string][]string）

```
slot[i] = {
    key:  go_string_ot {  // offset 0
        str: *char,        // 8 bytes
        len: int64         // 8 bytes
    },
    elem: go_slice_ot {  // offset 16
        array: *go_string_ot,  // 8 bytes
        len:   int64,          // 8 bytes
        cap:   int64           // 8 bytes
    }
}
// 总大小：40 bytes（可能需要对齐到 48 bytes）
```

### 内存对齐

在 64-bit 系统上：
- `uint64`、`uintptr`、指针：8 字节对齐
- `maps.Map` 结构体：字段按顺序排列，无 padding
  - `used` (0): 8 bytes
  - `seed` (8): 8 bytes
  - `dirPtr` (16): 8 bytes
  - `dirLen` (24): 4 bytes (int)
  - ...

## 文件修改清单

### Go 层修改

1. **ebpftracer/tls.go**
   - 添加 `maps.Map.dirPtr` 字段查找逻辑
   - 添加硬编码 fallback（Go 1.24+）
   - 允许在 `buckets` 失败时继续（只要 `goid` 成功）

### eBPF 层修改

1. **ebpftracer/ebpf/utrace/go/net/client.probe.bpf.c**
   - 新增 `inject_header_swiss()` 函数
   - 新增 `cw_inject_header_swiss()` 函数
   - 修改 `inject_header()` - 添加版本路由
   - 修改 `cw_inject_header()` - 添加版本路由
   - 修改 `cw_inject_header2()` - 添加版本路由
   - 修改 `cw_inject_header_half()` - 添加版本检查（暂不支持）

2. **ebpftracer/ebpf/utrace/go/net/server.probe.bpf.c**
   - 修改所有读取 `buckets_ptr_pos` 的地方，添加版本检查

## 已知限制

1. **仅支持小 Map**：当前实现只支持 `dirLen == 0` 的情况（小 map，≤8 个元素）
   - HTTP headers 通常是空或很小的 map，所以这个限制可以接受
   - 如果需要支持大 map，需要实现 directory/table 访问逻辑

2. **Slot 大小假设**：当前使用 40 字节（string 16 + []string 24）
   - 实际可能需要对齐到 48 字节
   - 需要根据测试结果调整

3. **H2 Hash**：当前使用固定值 `0x5A` 作为 control byte
   - 正确实现应该计算 key 的 hash，并使用 H2 位（低 7 位）
   - 对于 HTTP headers 注入，这个占位符可能足够

4. **并发安全**：Swiss Tables 有 `writing` 标志用于检测并发写入
   - 当前实现没有检查这个标志
   - 可能需要添加检查以确保安全

## 测试验证

### 成功指标

1. ✅ 成功获取 `maps.Map.dirPtr` 偏移量（16）
2. ✅ 代码编译通过
3. ⏳ 运行时 header 注入成功（需要实际测试）
4. ⏳ 注入的 header 能被正确读取（需要实际测试）

### 调试方法

查看 eBPF 日志：
```bash
# 方法 1：使用 bpftool
sudo bpftool prog tracelog

# 方法 2：使用 trace_pipe
sudo cat /sys/kernel/debug/tracing/trace_pipe | grep inject_header_swiss
```

关键日志点：
- `inject_header_swiss: START` - 函数开始
- `inject_header_swiss: used=X` - map 当前元素数
- `inject_header_swiss: dirLen=X` - map 类型
- `inject_header_swiss: group_ptr=0x...` - group 指针
- `inject_header_swiss: Found empty slot at index X` - 找到的空 slot
- `inject_header_swiss: SUCCESS` - 注入成功

## 参考资源

1. **Go 1.24 源码**：`/root/code/go/src/internal/runtime/maps/`
   - `map.go` - Map 结构体定义
   - `group.go` - Group 结构体定义
   - `table.go` - Table 结构体定义

2. **相关文档**：
   - [Abseil Swiss Tables](https://abseil.io/about/design/swisstables) - Swiss Tables 设计原理
   - Go 1.24 release notes - map 实现变更说明

## 后续优化建议

1. **实现正确的 H2 hash 计算**
   - 使用 Go 的 hash 算法计算 key 的 hash
   - 提取低 7 位作为 H2 值

2. **支持大 Map**
   - 实现 directory 访问逻辑
   - 处理 table 查找和 group 访问

3. **优化 Slot 大小检测**
   - 通过 DWARF 信息获取实际的 slot 大小
   - 或通过运行时探测确定

4. **添加并发安全检查**
   - 检查 `writing` 标志
   - 处理并发写入冲突

5. **性能优化**
   - 减少不必要的内存读取
   - 优化 control word 操作

## 总结

通过分析 Go 1.24 源码，理解了 Swiss Tables 的数据结构，实现了适配的 header 注入逻辑。主要工作包括：

1. ✅ 识别问题：`runtime.hmap.buckets` 字段不存在
2. ✅ 源码分析：理解 Swiss Tables 结构
3. ✅ 实现适配：新的 header 注入函数
4. ✅ 版本路由：自动选择正确的实现
5. ✅ 调试支持：详细的日志输出

代码已实现并可以编译，下一步需要实际测试验证功能是否正常工作。

