BY eHWRITTEN ON HALO19 MIN READ

MSP430 开发实录:WSL 环境下的 GCC 工具链、仿真踩坑与自动化测试复盘

MSP430 开发实录:WSL 环境下的 GCC 工具链、仿真踩坑与自动化测试复盘 1. 前言:脱离 IAR 的舒适区 在嵌入式开发中,IAR 和 CCS 是主流 IDE,但它们体积庞大且仅限于 Windows。本次项目的目标是在 WSL (Windows Subsystem for Linux)

MSP430 开发实录:WSL 环境下的 GCC 工具链、仿真踩坑与自动化测试复盘

1. 前言:脱离 IAR 的舒适区

在嵌入式开发中,IAR 和 CCS 是主流 IDE,但它们体积庞大且仅限于 Windows。本次项目的目标是在 WSL (Windows Subsystem for Linux) 环境下,构建一套轻量级、命令行驱动的 MSP430 开发工作流。

项目目标:实现一个具备走时、校时、闹钟功能的电子表。 核心挑战:环境配置、代码架构优化、无实物仿真验证。


2. 软件架构重构:从“延时”到“状态机”

2.1 痛点分析

初版代码使用了大量的 delay_ms() 进行按键消抖和流程控制。

  • 问题:CPU 在延时函数中空转,无法响应突发事件(如闹钟响铃时按键停止),导致逻辑阻塞。
  • 隐患sprintf 格式化字符串时未考虑 \0 结束符,导致 char buf[16] 溢出警告(Off-by-one error)。

2.2 架构优化:时间片轮询 + 有限状态机 (FSM)

重构后的代码采用了前后台系统思想:

  • 后台 (ISR):TimerA 产生 1ms 心跳 (Tick),负责全局时间计数、按键状态扫描。
  • 前台 (Main Loop):基于状态机处理业务逻辑,无任何阻塞延时。

状态机设计:

typedef enum {
    STATE_RUNNING = 0,    // 正常走时
    STATE_SET_HOUR,       // 调时
    STATE_SET_MIN,        // 调分
    STATE_SET_ALARM_HOUR, // 调闹钟时
    STATE_SET_ALARM_MIN,  // 调闹钟分
    STATE_ALARM_SWITCH    // 闹钟开关
} SystemState_t;

非阻塞按键消抖逻辑: 不再死等 20ms,而是利用 1ms 中断计数。

  • cnt < 20:认为是抖动,不处理。
  • cnt >= 20state == 0:确认为按下,触发逻辑,置 state = 1
  • IO 恢复高电平:清零 cntstate,准备下一次触发。

3. 工具链与环境配置 (WSL + GCC)

3.1 编译器选型

不再使用老旧的 gcc-msp430 (apt源),而是选用 TI 官方提供的 msp430-elf-gcc (基于 GCC 9.3+)。

关键配置点: TI 将编译器与支持文件(头文件、链接脚本)分离,编译时必须显式指定路径:

msp430-elf-gcc -I ./include -L ./include -mmcu=msp430g2553 ...

3.2 自动化构建 (Makefile)

为了避免重复输入长命令,编写 Makefile 实现工程化管理:

  • make:自动编译 .c -> .elf -> .hex
  • msp430-elf-size:自动打印 Flash/RAM 占用情况,监控资源。
  • 技巧:GCC 的 __delay_cycles() 是内联函数,参数必须是编译期常数,因此在代码中需使用宏定义 #define Delay_US(x) __delay_cycles(x) 封装。

4. 调试器踩坑录:GDB vs MSPDebug

这是本次开发中耗时最多的环节,主要涉及 Linux 库兼容性和模拟器缺陷。

4.1 坑一:GDB 的“库依赖地狱”

运行 msp430-elf-gdb 时报错:

error while loading shared libraries: libncursesw.so.5

原因:TI 提供的 GDB 编译于较旧的 Linux 环境,依赖 .so.5 版本,而现代 Ubuntu (22.04+) 默认为 .so.6解决:通过软链接“欺骗”加载器(虽不优雅但有效):

sudo ln -s /usr/lib/.../libncursesw.so.6 /usr/lib/.../libncursesw.so.5
sudo ln -s /usr/lib/.../libtinfo.so.6 /usr/lib/.../libtinfo.so.5

4.2 坑二:GDB Simulator 的复位向量 Bug

在 GDB 中执行 target simrun 后报错:

Fault: PC(0) is less than 0x10

原因:GDB 内置模拟器在加载 ELF 时,未能正确读取复位向量(地址 0xFFFE),导致 PC 指针初始化为 0x0000 非法地址。 尝试修复:手动设置 PC (set $pc = 0xc280) 或手动写入复位向量 (set *(short*)0xfffe = ...)。虽然能强行启动,但定时器中断逻辑依然表现异常。

4.3 最终方案:转向 mspdebug

放弃 GDB 的内置模拟器,改用专门针对 MSP430 优化的 mspdebug

  • 优势:仿真稳定,指令集支持完善,无库依赖问题。
  • 命令mspdebug sim -> prog main.elf -> run

5. “上帝模式”仿真技巧

在没有硬件(无开发板、无示波器)的情况下,如何验证“23:59:59 -> 00:00:00”或“闹钟触发”逻辑? 答案是:直接修改内存 (Memory Manipulation)

我们需要用到两个核心工具:

  1. nm (Name List):查看变量的内存地址。
    • 例:g_sec -> 0x02d6
  2. md/mw (Memory Dump/Write):读写内存。

逻辑验证三部曲

  1. 篡改时间:利用 mw 将秒数改为 59,分钟改为 29。
  2. 伪造中断:利用 mwg_flag_1s 标志位置 1(欺骗主循环 Timer 已触发)。
  3. 单步执行:执行 runstep,观察 g_min 是否自动进位,g_is_ringing 是否置位。

这种方法跳过了漫长的等待时间,直接对边界条件进行测试。


6. 自动化测试脚本 (Python) 与“启动代码”陷阱

为了将手动测试流程自动化,编写了 Python 脚本调用 mspdebug。在此过程中遇到了一个极具隐蔽性的 Bug。

6.1 脚本逻辑 V1 (失败)

commands = [
    "prog main.elf",
    "mw 0x0203 0C",  # 设置时间
    "mw 0x02d5 01",  # 开启闹钟
    "step 1000"      # 运行
]

现象:脚本运行后,内存中的变量值依然是默认值,闹钟未响。

6.2 根因分析:C 语言启动代码 (C-Startup)

单片机复位后,并不会立即执行 main 函数,而是先执行一段汇编启动代码(Startup Code)。

  • 职责:初始化栈指针 (SP),初始化全局变量 (.data 搬运, .bss 清零)。
  • 冲突:脚本在 prog 后立即写入内存,此时 CPU 刚开始跑启动代码。随后的几百个周期内,启动代码执行初始化,将我们写入的“作弊数据”全部覆盖/清零了

6.3 脚本逻辑 V2 (成功)

引入 断点 (Breakpoint) 机制,确保初始化完成后再介入。

commands = [
    "prog main.elf",
    # 核心修正:设置断点在业务逻辑函数 Process_Time
    f"setbreak 0x{var_addr['Process_Time']}", 
    "run",        # 全速运行,跑完启动代码和 LCD 初始化
    f"delbreak 0x{var_addr['Process_Time']}", # 命中后删除断点
    
    # 此时环境已稳定,可以安全修改内存
    f"mw 0x{var_addr['g_sec']} 3B", 
    ...
]

#include <msp430.h>
#include <stdio.h>

/* ================= 硬件定义 ================= */
#define LCD_DATA_OUT    P2OUT
#define LCD_DATA_DIR    P2DIR
#define LCD_CTRL_OUT    P3OUT
#define LCD_CTRL_DIR    P3DIR
#define LCD_RS          BIT0
#define LCD_RW          BIT1
#define LCD_EN          BIT2

// 按键引脚
#define KEY_PORT_IN     P1IN
#define KEY_PORT_DIR    P1DIR
#define KEY_PORT_REN    P1REN
#define KEY_PORT_OUT    P1OUT
#define KEY_MODE_PIN    BIT0
#define KEY_ADD_PIN     BIT1

// --- 新增:蜂鸣器/LED 引脚 (P1.6) ---
// 如果你是外接有源蜂鸣器,请接在 P1.6,低电平触发还是高电平触发请根据 Update_Buzzer 修改
#define BUZZER_PIN      BIT6 

// 宏定义延时
#define Delay_US_Hardware(x) __delay_cycles(x)

/* ================= 状态机定义 ================= */
typedef enum {
    STATE_RUNNING = 0,    // 正常走时
    STATE_SET_HOUR,       // 调时间:时
    STATE_SET_MIN,        // 调时间:分
    STATE_SET_ALARM_HOUR, // 调闹钟:时
    STATE_SET_ALARM_MIN,  // 调闹钟:分 
    STATE_ALARM_SWITCH    // 闹钟开关   
} SystemState_t;

volatile SystemState_t current_state = STATE_RUNNING;

/* ================= 全局变量 ================= */
// 时间变量
volatile unsigned char g_hour = 12;
volatile unsigned char g_min = 0;
volatile unsigned char g_sec = 0;

// --- 新增:闹钟变量 ---
volatile unsigned char g_alarm_hour = 6;  // 默认早晨6点
volatile unsigned char g_alarm_min  = 30;
volatile unsigned char g_alarm_on   = 0;  // 0:关, 1:开
volatile unsigned char g_is_ringing = 0;  // 0:未响, 1:正在响铃

// 系统标志位
volatile unsigned int  g_ms_counter = 0;     // 毫秒计数
volatile unsigned char g_flag_1s = 0;        // 1秒标志
volatile unsigned char g_display_update = 1; // 屏幕刷新请求

// 按键相关变量
#define KEY_DEBOUNCE_TICKS 20 // 20ms 消抖
unsigned char key_mode_cnt = 0;
unsigned char key_add_cnt = 0;
unsigned char key_mode_state = 0; // 0:松开, 1:按下
unsigned char key_add_state = 0;

/* ================= 函数声明 ================= */
void System_Init(void);
void LCD_Init(void);
void LCD_Write_Cmd(unsigned char cmd);
void LCD_Write_Data(unsigned char dat);
void LCD_ShowString(unsigned char x, unsigned char y, char *str);
void Process_Keys(void);
void Process_Time(void);
void Update_Display(void);
void Alarm_Check(void); // 新增:闹钟检查

/* ================= 主函数 ================= */
int main(void)
{
    WDTCTL = WDTPW + WDTHOLD; // 关狗
    System_Init();
    LCD_Init();

    LCD_ShowString(0, 0, "MSP430 Clock");

    while(1)
    {
        // 1. 处理按键
        Process_Keys();

        // 2. 处理时间逻辑 (1s一次)
        Process_Time();

        // 3. 闹钟响铃处理 (如果正在响铃,让蜂鸣器滴滴响)
        if(g_is_ringing)
        {
            // 利用 g_ms_counter 制造简单的 500ms 闪烁/蜂鸣效果
            if((g_ms_counter / 500) % 2 == 0) 
                P1OUT |= BUZZER_PIN;  // 响/亮
            else 
                P1OUT &= ~BUZZER_PIN; // 停/灭
        }
        else
        {
            P1OUT &= ~BUZZER_PIN; // 确保关闭
        }

        // 4. 刷新显示
        if(g_display_update)
        {
            Update_Display();
            g_display_update = 0; 
        }
    }
}

/* ================= 定时器中断 (系统心跳 1ms) ================= */
__attribute__((interrupt(TIMER0_A0_VECTOR)))
void Timer_A (void)
{
    // --- 1. 全局时间计数 ---
    g_ms_counter++;
    if(g_ms_counter >= 1000)
    {
        g_ms_counter = 0;
        // 正常走时模式下,且没有在响铃(防止响铃时时间乱跳虽然通常不影响),才走时
        if(current_state == STATE_RUNNING) {
            g_flag_1s = 1; 
        }
    }

    // --- 2. 按键扫描 (非阻塞消抖) ---
    // Check Mode Key
    if ((KEY_PORT_IN & KEY_MODE_PIN) == 0) { 
        if (key_mode_cnt < KEY_DEBOUNCE_TICKS) key_mode_cnt++;
    } else { 
        key_mode_cnt = 0;
        key_mode_state = 0; 
    }

    // Check Add Key
    if ((KEY_PORT_IN & KEY_ADD_PIN) == 0) {
        if (key_add_cnt < KEY_DEBOUNCE_TICKS) key_add_cnt++;
    } else {
        key_add_cnt = 0;
        key_add_state = 0;
    }
}

/* ================= 逻辑处理函数 ================= */

void Process_Keys(void)
{
    // === 优先级最高:如果闹钟正在响,任意键停止闹钟 ===
    if (g_is_ringing)
    {
        // 检测任意键按下
        if ((key_mode_cnt >= KEY_DEBOUNCE_TICKS && key_mode_state == 0) || 
            (key_add_cnt >= KEY_DEBOUNCE_TICKS && key_add_state == 0))
        {
            key_mode_state = 1; // 消耗掉按键
            key_add_state = 1;
            g_is_ringing = 0;   // 停止响铃
            P1OUT &= ~BUZZER_PIN; // 立即关闭蜂鸣器
            g_display_update = 1;
            return; // 不执行后续逻辑
        }
    }

    // Mode 键逻辑:切换状态
    if (key_mode_cnt >= KEY_DEBOUNCE_TICKS && key_mode_state == 0)
    {
        key_mode_state = 1;
        
        // 状态流转: 
        // Running -> Set Time H -> Set Time M -> Set Alarm H -> Set Alarm M -> Alarm SW -> Running
        switch(current_state)
        {
            case STATE_RUNNING:        current_state = STATE_SET_HOUR;       break;
            case STATE_SET_HOUR:       current_state = STATE_SET_MIN;        break;
            case STATE_SET_MIN:        current_state = STATE_SET_ALARM_HOUR; break; // 新增跳转
            case STATE_SET_ALARM_HOUR: current_state = STATE_SET_ALARM_MIN;  break;
            case STATE_SET_ALARM_MIN:  current_state = STATE_ALARM_SWITCH;   break;
            case STATE_ALARM_SWITCH:   current_state = STATE_RUNNING;        break;
            default:                   current_state = STATE_RUNNING;        break;
        }
        g_display_update = 1;
    }

    // Add 键逻辑:增加数值
    if (key_add_cnt >= KEY_DEBOUNCE_TICKS && key_add_state == 0)
    {
        key_add_state = 1; 

        switch(current_state)
        {
            case STATE_SET_HOUR:
                g_hour++; if (g_hour >= 24) g_hour = 0;
                break;
            case STATE_SET_MIN:
                g_min++; if (g_min >= 60) g_min = 0;
                g_sec = 0;
                break;
            // --- 新增闹钟设置逻辑 ---
            case STATE_SET_ALARM_HOUR:
                g_alarm_hour++; if(g_alarm_hour >= 24) g_alarm_hour = 0;
                break;
            case STATE_SET_ALARM_MIN:
                g_alarm_min++; if(g_alarm_min >= 60) g_alarm_min = 0;
                break;
            case STATE_ALARM_SWITCH:
                g_alarm_on = !g_alarm_on; // 切换 0/1
                break;
            default: break;
        }
        g_display_update = 1;
    }
}

void Process_Time(void)
{
    if(g_flag_1s)
    {
        g_flag_1s = 0;
        
        g_sec++;
        if(g_sec >= 60)
        {
            g_sec = 0;
            g_min++;
            // 每分钟检查一次闹钟(如果要在00秒准时响,可以在这里检查)
            Alarm_Check(); 
            
            if(g_min >= 60)
            {
                g_min = 0;
                g_hour++;
                if(g_hour >= 24) g_hour = 0;
            }
        }
        g_display_update = 1;
    }
}

// 检查闹钟是否该响了
void Alarm_Check(void)
{
    if(g_alarm_on == 1) // 闹钟开启
    {
        if(g_hour == g_alarm_hour && g_min == g_alarm_min)
        {
            g_is_ringing = 1; // 触发响铃
        }
    }
}

// 屏幕刷新逻辑
void Update_Display(void)
{
    char buf[20];
    
    // 清屏或覆盖显示是很重要的,这里通过写满16个字符来覆盖旧内容
    switch(current_state)
    {
        case STATE_RUNNING:
            LCD_ShowString(0, 0, "Mode: Running   ");
            // 在正常模式下显示闹钟状态 (例如右上角显示 "AL")
            if(g_alarm_on) LCD_ShowString(14, 0, "AL"); 
            else           LCD_ShowString(14, 0, "  ");
            
            sprintf(buf, "Time: %02d:%02d:%02d", g_hour, g_min, g_sec);
            LCD_ShowString(0, 1, buf);
            break;

        case STATE_SET_HOUR:
            LCD_ShowString(0, 0, "Set Time Hour   ");
            sprintf(buf, "      %02d        ", g_hour);
            LCD_ShowString(0, 1, buf);
            break;

        case STATE_SET_MIN:
            LCD_ShowString(0, 0, "Set Time Min    ");
            sprintf(buf, "      %02d        ", g_min);
            LCD_ShowString(0, 1, buf);
            break;

        // --- 新增界面 ---
        case STATE_SET_ALARM_HOUR:
            LCD_ShowString(0, 0, "Set Alarm Hour  ");
            sprintf(buf, "      %02d        ", g_alarm_hour);
            LCD_ShowString(0, 1, buf);
            break;

        case STATE_SET_ALARM_MIN:
            LCD_ShowString(0, 0, "Set Alarm Min   ");
            sprintf(buf, "      %02d        ", g_alarm_min);
            LCD_ShowString(0, 1, buf);
            break;

        case STATE_ALARM_SWITCH:
            LCD_ShowString(0, 0, "Set Alarm SW    ");
            if(g_alarm_on) LCD_ShowString(0, 1, "      ON        ");
            else           LCD_ShowString(0, 1, "      OFF       ");
            break;
    }
}

/* ================= 底层驱动函数 ================= */

void System_Init(void)
{
    // 配置时钟 1MHz
    if (CALBC1_1MHZ==0xFF) return; 
    DCOCTL = 0;
    BCSCTL1 = CALBC1_1MHZ;
    DCOCTL = CALDCO_1MHZ;

    // 配置IO - 按键
    KEY_PORT_DIR &= ~(KEY_MODE_PIN | KEY_ADD_PIN);
    KEY_PORT_REN |= (KEY_MODE_PIN | KEY_ADD_PIN); 
    KEY_PORT_OUT |= (KEY_MODE_PIN | KEY_ADD_PIN); 

    // 配置IO - 蜂鸣器 (P1.6 输出)
    P1DIR |= BUZZER_PIN;
    P1OUT &= ~BUZZER_PIN; // 初始关闭

    // 配置定时器 Timer0_A
    TA0CCTL0 = CCIE;            
    TA0CCR0 = 1000 - 1;         
    TA0CTL = TASSEL_2 + MC_1;   
    
    __bis_SR_register(GIE);    
}

void LCD_Write_Cmd(unsigned char cmd)
{
    LCD_CTRL_OUT &= ~LCD_RS; 
    LCD_CTRL_OUT &= ~LCD_RW; 
    LCD_DATA_OUT = cmd;
    LCD_CTRL_OUT |= LCD_EN;  
    Delay_US_Hardware(1000); 
    LCD_CTRL_OUT &= ~LCD_EN; 
}

void LCD_Write_Data(unsigned char dat)
{
    LCD_CTRL_OUT |= LCD_RS;  
    LCD_CTRL_OUT &= ~LCD_RW; 
    LCD_DATA_OUT = dat;
    LCD_CTRL_OUT |= LCD_EN;  
    Delay_US_Hardware(1000); 
    LCD_CTRL_OUT &= ~LCD_EN; 
}

void LCD_Init(void)
{
    LCD_DATA_DIR = 0xFF; 
    LCD_CTRL_DIR |= (LCD_RS | LCD_RW | LCD_EN);
    
    LCD_CTRL_OUT &= ~LCD_RW;
    LCD_CTRL_OUT &= ~LCD_EN;
    
    Delay_US_Hardware(20000); 
    LCD_Write_Cmd(0x38);
    Delay_US_Hardware(5000);
    LCD_Write_Cmd(0x38);
    Delay_US_Hardware(5000);
    LCD_Write_Cmd(0x0C); 
    LCD_Write_Cmd(0x06);
    LCD_Write_Cmd(0x01); 
    Delay_US_Hardware(5000);
}

void LCD_ShowString(unsigned char x, unsigned char y, char *str)
{
    unsigned char addr;
    if (y == 0) addr = 0x80 + x;
    else addr = 0xC0 + x;
    LCD_Write_Cmd(addr);
    while (*str)
    {
        LCD_Write_Data(*str);
        str++;
    }
}
import subprocess
import re
import time

# ================= 配置部分 =================
ELF_FILE = "main.elf"
MSPDEBUG_CMD = "mspdebug sim"

# 我们需要查找的变量名 + 关键函数名
TARGET_VARS = [
    "g_hour", "g_min", "g_sec", 
    "g_alarm_hour", "g_alarm_min", "g_alarm_on", 
    "g_flag_1s", "g_is_ringing",
    "Process_Time"  # <--- 【关键】我们要在这个函数打断点
]

# ================= 第一步:自动查户口 =================
print(f"[*] 正在扫描 {ELF_FILE} 的内存地址...")
try:
    nm_output = subprocess.check_output(["msp430-elf-nm", ELF_FILE]).decode("utf-8")
except Exception as e:
    print("[-] 错误:无法运行 msp430-elf-nm,请确认已安装。")
    exit(1)

var_addr = {}
for line in nm_output.splitlines():
    parts = line.split()
    if len(parts) >= 3:
        # nm输出格式: 地址 类型 名字 (如: 0000c49e T Process_Time)
        addr, type_, name = parts[0], parts[1], parts[2]
        if name in TARGET_VARS:
            var_addr[name] = addr 

# 检查是否找全了
if len(var_addr) != len(TARGET_VARS):
    print("[-] 警告:部分符号未找到 (可能是编译优化导致函数内联)。")
    print(f"    找到: {list(var_addr.keys())}")
    # 这里不强制退出,防止某些变量名对不上
else:
    print(f"[+] 成功获取 {len(var_addr)} 个符号地址。")

# ================= 第二步:构造“必胜”指令流 =================
commands = [
    # 1. 加载程序
    "prog main.elf",
    
    # 2. 【核心大招】设置断点!
    # 告诉 CPU:别管前面初始化跑多久,直到运行到 Process_Time 函数再叫我
    f"setbreak 0x{var_addr['Process_Time']}",
    
    # 3. 狂奔吧!
    # 这一步会跑过所有的 LCD_Init 延时,直到断点触发
    "run",
    
    # 4. 到了断点,先删除断点 (防止后面 step 的时候一直卡住)
    f"delbreak 0x{var_addr['Process_Time']}",
    
    # --- 现在程序停在主循环里了,环境安全,开始作弊 ---
    
    # 5. 设置时间 12:29:59
    f"mw 0x{var_addr['g_hour']} 0C",
    f"mw 0x{var_addr['g_min']}  1D",
    f"mw 0x{var_addr['g_sec']}  3B",
    
    # 6. 设置闹钟 12:30 且开启
    f"mw 0x{var_addr['g_alarm_hour']} 0C",
    f"mw 0x{var_addr['g_alarm_min']}  1E",
    f"mw 0x{var_addr['g_alarm_on']}   01",
    
    # 7. 强制触发1秒心跳 (骗程序去执行进位逻辑)
    f"mw 0x{var_addr['g_flag_1s']}    01",
    
    # 8. 再跑一点点 (让程序执行 Process_Time 里的逻辑)
    # 因为我们就在 Process_Time 门口,跑 500 步足够进去了
    "step 500",  
    
    # 9. 检查结果
    f"md 0x{var_addr['g_is_ringing']} 1",
    
    "exit"
]

input_str = "\n".join(commands) + "\n"

# ================= 第三步:自动运行仿真 =================
print("\n[*] 正在启动 mspdebug (智能断点模式)...")

process = subprocess.Popen(
    MSPDEBUG_CMD.split(),
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True
)

stdout, stderr = process.communicate(input=input_str)

# ================= 第四步:分析结果 =================
target_addr_short = var_addr['g_is_ringing'][-4:]
pattern = re.compile(f"{target_addr_short}:\\s+([0-9a-fA-F]{{2}})")
match = pattern.search(stdout)

if match:
    val = match.group(1)
    print("\n" + "="*30)
    if val == "01":
        print(f"✅ 测试通过!闹钟成功触发。")
        print(f"   内存值: 0x{var_addr['g_is_ringing']} = {val}")
    else:
        print(f"❌ 测试失败!闹钟未响。")
        print(f"   内存值: 0x{var_addr['g_is_ringing']} = {val}")
    print("="*30)
else:
    print("[-] 无法解析输出,可能 mspdebug 崩溃或未断下。")
    ```