execve_hook/TEST_DEBUG.md

5.1 KiB

DEBUG 模式测试说明

问题描述

之前遇到的 "connection reset by peer" 错误的根本原因是:

  1. TCP 流式传输特性: 数据可能分多次到达,不能假设一次 read() 就能读到完整数据
  2. 部分读取问题: 原代码只调用一次 read(),导致读取不完整的消息头或载荷
  3. 错误表现: 客户端读取消息头时只收到 12 字节(期望 16 字节),导致解析失败并关闭连接

修复方案

C 代码修复 (socket_protocol.c)

  1. 添加 read_full() 函数: 循环读取直到获取完整的指定字节数

    static ssize_t read_full(int sock, void* buf, size_t count) {
        // 循环读取,处理 EINTR 和部分读取
        // 确保读取到 count 字节或连接关闭
    }
    
  2. 添加 write_full() 函数: 循环写入直到发送完整的指定字节数

    static ssize_t write_full(int sock, const void* buf, size_t count) {
        // 循环写入,处理 EINTR 和部分写入
        // 确保写入 count 字节
    }
    
  3. 修改 read_message(): 使用 read_full() 读取消息头和载荷

  4. 修改 write_message(): 使用 write_full() 发送消息头和载荷

Go 代码修复 (protocol.go)

  1. 使用 io.ReadFull(): 确保完整读取消息头和载荷

    // 读取消息头
    headerBuf := make([]byte, 16)
    if _, err := io.ReadFull(conn, headerBuf); err != nil {
        return 0, nil, err
    }
    
    // 读取载荷
    if _, err := io.ReadFull(conn, payload); err != nil {
        return 0, nil, err
    }
    
  2. 完整写入: 构建完整缓冲区后循环写入

    written := 0
    for written < len(buf) {
        n, err := conn.Write(buf[written:])
        if err != nil {
            return err
        }
        written += n
    }
    

DEBUG 日志系统

C 代码 DEBUG 功能

  1. debug.hdebug.c:

    • DEBUG_LOG(fmt, ...): 带时间戳、文件位置的日志
    • DEBUG_HEX(prefix, data, len): 十六进制数据转储
    • 只在 DEBUG=1 编译时启用
  2. 关键日志点:

    • 消息发送/接收的开始和完成
    • 消息头和载荷的十六进制转储(前64字节)
    • 读写进度(部分读取时)
    • 终端模式切换
    • 连接生命周期事件

编译和测试

# 1. 编译 DEBUG 模式的 C 客户端
cd /home/qcqcqc/workspace/Projects/bash_smart/execve_hook
make clean
make DEBUG=1 test_socket_client

# 2. 在一个终端启动 Go 服务端
cd /home/qcqcqc/workspace/Projects/bash_smart/go_service
sudo GOPROXY=https://goproxy.cn,direct go run ./cmd/tests/test_socket_terminal/progress-animated/main.go

# 3. 在另一个终端运行 DEBUG 客户端
cd /home/qcqcqc/workspace/Projects/bash_smart/execve_hook
./build/test_socket_client 2> debug.log

# 4. 查看 DEBUG 日志
tail -f debug.log

DEBUG 日志示例

[DEBUG 16:54:33.123] [src/client.c:320:seeking_solutions] 开始 seeking_solutions: filename=/usr/bin/ls
[DEBUG 16:54:33.124] [src/client.c:355:seeking_solutions] 尝试连接到 /var/run/bash-smart.sock
[DEBUG 16:54:33.125] [src/client.c:365:seeking_solutions] 连接成功
[DEBUG 16:54:33.126] [src/socket_protocol.c:35:write_message] 写入消息: type=1, payload_len=1024
[DEBUG HEX] 消息头 (16 bytes):
54 4d 53 42 01 00 00 00 00 04 00 00 00 00 00 00
[DEBUG 16:54:33.127] [src/socket_protocol.c:65:write_full] 进度 1024/1024 字节
[DEBUG 16:54:33.128] [src/socket_protocol.c:76:read_message] 开始读取消息...
[DEBUG 16:54:33.129] [src/socket_protocol.c:96:read_full] 进度 16/16 字节
[DEBUG HEX] 收到消息头 (16 bytes):
54 4d 53 42 03 00 00 00 6c 03 00 00 00 00 00 00
[DEBUG 16:54:33.130] [src/socket_protocol.c:100:read_message] 消息类型=3, 载荷长度=876

验证修复

修复后应该看到:

  1. 所有消息完整读取,没有 "Failed to read message header" 错误
  2. 连接正常完成,TUI 显示完整流程
  3. 终端正确恢复,光标在新行开始
  4. DEBUG 日志显示所有读写操作成功完成

技术要点

为什么需要完整读写?

  1. TCP 流特性: TCP 是字节流协议,不保证消息边界
  2. 内核缓冲区: 数据可能在内核缓冲区中分批到达
  3. 网络延迟: 跨网络通信时更容易出现部分读取
  4. 信号中断: EINTR 会导致系统调用提前返回

read_full() vs read()

函数 行为 适用场景
read() 返回当前可用的数据,可能少于请求的字节数 不确定数据长度的场景
read_full() 循环读取直到获取完整的请求字节数或错误 已知数据长度的协议(如我们的消息协议)

Go 的 io.ReadFull()

Go 标准库提供了 io.ReadFull(),其行为等同于我们实现的 read_full():

  • 返回 io.ErrUnexpectedEOF 如果只读取了部分数据就遇到 EOF
  • 返回 io.EOF 如果一个字节都没读到就遇到 EOF
  • 只在成功读取完整字节数时返回 nil 错误

后续改进

  1. 添加读写超时机制
  2. 添加消息大小限制(防止内存攻击)
  3. 实现消息队列缓冲
  4. 添加压缩支持(大载荷)
  5. 添加加密支持(敏感数据)