当前位置: 首页 > news >正文

嵌入式安全测试实战:CPU寄存器、栈与看门狗自检详解

1. 项目概述:嵌入式安全测试的基石

在嵌入式系统,尤其是家电、工业控制这类对可靠性要求极高的领域,代码能跑起来只是第一步,如何确保它在长达数年甚至十几年的生命周期里,面对电磁干扰、温度变化、器件老化等挑战时依然“不出错”,才是真正的难题。这背后依赖的,就是功能安全(Functional Safety)体系。IEC 60730标准,特别是其附录H,为这类设备的微控制器软件自检提供了明确的“体检清单”。它要求系统必须具备自我诊断能力,能主动发现CPU、内存、时钟等核心硬件的随机硬件故障。

NXP等芯片原厂提供的IEC60730安全库,就是一套现成的“体检工具包”。它把标准里那些抽象的要求,变成了一个个可以直接调用的C函数或汇编模块。今天,我们就深入这个工具包的内核,拆解其中最基础也最关键的三个“体检项目”:CPU特殊寄存器测试、应用程序栈测试以及看门狗功能验证。这些测试构成了系统安全运行的“铁三角”——CPU是大脑,栈是工作记忆区,看门狗是最后的守护者。理解它们的实现原理和调用细节,是设计高可靠嵌入式系统的必修课。

2. CPU特殊寄存器测试:守护系统的“优先级防火墙”

在基于ARM Cortex-M4/M7内核的微控制器中,BASEPRIFAULTMASK是两个至关重要的特殊功能寄存器,它们共同构成了异常和中断处理的“优先级防火墙”。BASEPRI寄存器用于屏蔽所有优先级低于某个特定值的可屏蔽中断,而FAULTMASK则能直接屏蔽除NMI(不可屏蔽中断)外的所有异常,包括硬Fault。在安全关键应用中,必须确保这两个寄存器能够被正确写入和读出,其功能没有因潜在的硬件故障(如锁存器位翻转)而失效。

2.1 测试原理与模式设计

库中提供的FS_CM4_CM7_CPU_SpecialRegistersFS_CM4_CM7_CPU_Special8PriorityLevels函数,正是基于这个目的。它们的测试逻辑非常直接,属于“读写回读验证”模式:

  1. 写入特定模式:函数首先将一组预设的测试模式(Test Pattern)写入目标寄存器。例如,对于BASEPRI,测试模式可能是0xA00x50;对于FAULTMASK,则是0x10x0
  2. 立即读回:写入操作后,立即从该寄存器读回当前值。
  3. 结果比对:将读回的值与之前写入的预期值进行严格比较。
  4. 状态返回:如果所有比对都一致,函数返回FS_PASS;只要有任何一位不匹配,则返回FS_FAIL_CPU_SPECIAL,指示CPU特殊寄存器功能故障。

这里有一个关键细节:为什么测试模式要选择0xA00x500x40这样的值?这并非随意选择。以0xA0(二进制1010 0000)和0x50(二进制0101 0000)为例,它们是一对“互补”的测试向量。0xA0在bit7和bit5为高,0x50在bit6和bit4为高。这种“走1”和“走0”的测试,能够有效地检测寄存器中每个比特位的“粘滞”故障(Stuck-at fault),即某个位永远为1或永远为0。同时,这些值也符合ARM架构中BASEPRI寄存器对优先级字段编码的约束(通常只使用高几位)。

注意FS_CM4_CM7_CPU_Special8PriorityLevels函数是专门为中断优先级只有8个等级(3位编码)的设备准备的。在这种情况下,BASEPRI的有效位可能更少,因此测试模式(如0xA00x40)也需要相应调整,以确保测试的是有效的位域,避免因写入无效优先级值而引发不可预期的行为。

2.2 栈指针寄存器测试:最后的防线

相较于BASEPRIFAULTMASK,栈指针(Stack Pointer)寄存器的测试更为关键,也更具挑战性。Cortex-M内核有两个栈指针:MSP(主栈指针)和PSP(进程栈指针)。安全库分别提供了FS_CM4_CM7_CPU_SPmain(测试MSP)和FS_CM4_CM7_CPU_SPprocess(测试PSP)函数。

它们的测试逻辑与特殊寄存器类似,也是写入测试模式(如0x555555540xAAAAAAA8)并读回验证。但有一个根本性的不同:栈指针测试函数不能被中断。原因在于,如果在测试过程中发生中断,CPU会自动将多个寄存器压入当前栈,这必然会改变栈指针的值,导致测试失败。因此,在调用这两个函数前,通常需要先关闭全局中断。

最需要关注的是其错误处理机制。函数原型文档中明确指出:“If SP_main/SP_process is corrupted, the function is in an endless loop with the interrupts disabled.”这句话信息量极大。

  • 为什么是死循环?当检测到栈指针损坏时,系统的栈空间可能已经混乱,任何函数调用、局部变量访问都可能引发总线错误或内存访问违例,导致系统进入不可恢复的状态。此时,最安全的做法不是尝试“修复”(因为已无可靠执行环境),而是立即“停车”。
  • 为什么关闭中断?防止任何异步事件打断这个死循环,确保系统稳定地停留在已知的错误状态。
  • 然后呢?文档紧接着说:“This state must be observed by another safety mechanism (for example, watchdog).” 这就是安全设计的“双保险”思路。CPU自检函数负责检测故障并进入安全状态(死循环),而独立的看门狗定时器则负责监控这个状态——当系统因故障卡死在循环中,无法按时“喂狗”时,看门狗将触发系统复位,尝试从根本错误中恢复。

2.3 实操要点与性能考量

在实际集成这些测试时,你需要关注以下几点:

  1. 调用时机与频率:这些测试属于“在线自检”,需要在系统运行时周期性执行。通常放在主循环或低优先级后台任务中,执行频率需根据安全目标(如诊断覆盖率、故障容忍时间间隔)来确定。对于栈指针测试,由于其需要关中断,应放在对实时性影响最小的时机(如任务空闲时)快速执行。
  2. 测试顺序:建议先进行栈指针测试,再进行其他寄存器测试。因为栈指针是其他函数调用(包括测试函数本身)的基础,确保栈指针有效是后续一切操作的前提。
  3. 性能开销:文档提供了每个函数的执行周期数和大致时间(基于特定主频)。例如,FS_CM4_CM7_CPU_SPprocess约51个周期(0.638 µs @ 80MHz)。这个开销非常小,但当你需要测试数十个寄存器时,累积时间仍需纳入系统实时性预算。
  4. 结果处理:绝不能忽略返回值。一旦收到FS_FAIL_CPU_SPECIAL,必须立即跳转到预设的安全错误处理函数。这个函数的具体行为由应用定义,可能是记录错误日志、点亮故障灯、切断负载电源,并最终触发系统复位。

3. 栈测试:为内存划出“警戒区”

如果说CPU寄存器是大脑的指令中心,那么栈(Stack)就是大脑的“工作记忆区”。所有的局部变量、函数调用地址、中断上下文都存放在这里。栈溢出(Stack Overflow)或下溢(Underflow)是嵌入式系统最隐蔽、最致命的错误之一,它可能悄无声息地覆盖相邻的数据或代码,导致程序跑飞,且极难追踪。

IEC60730库提供的栈测试方案,其核心思想不是去测试栈内部每一个字节(那是变量内存测试的任务),而是在栈区域的上下边界外,设立“哨兵区”或“警戒区”

3.1 链接器配置:定义安全边界

这是整个栈测试中最关键、也最容易出错的一步——在链接脚本(Linker Script)中为栈的上下方预留出专用的测试内存块。以提供的IAR链接器配置文件(.icf)示例为例:

define symbol __ICFEDIT_size_cstack__ = 512; /* 应用程序栈大小 */ define exported symbol STACK_TEST_BLOCK_SIZE = 0x10; /* 每个哨兵区大小,必须是4的倍数 */ /* 计算哨兵区和栈的地址 */ define exported symbol STACK_TEST_P_4 = __region_RAM2_end__ - 0x3; define exported symbol STACK_TEST_P_3 = STACK_TEST_P_4 - STACK_TEST_BLOCK_SIZE +0x4; define exported symbol __BOOT_STACK_ADDRESS = STACK_TEST_P_3 - 0x4; define exported symbol STACK_TEST_P_2 = __BOOT_STACK_ADDRESS - __ICFEDIT_size_cstack__ -0x4; define exported symbol STACK_TEST_P_1 = STACK_TEST_P_2 - STACK_TEST_BLOCK_SIZE; /* 将哨兵区从常规RAM区域中排除,防止编译器分配变量到此 */ define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __region_RAM2_end__] - mem:[from STACK_TEST_P_1 size 0x10] - mem:[from STACK_TEST_P_3 size 0x10];

经过这样定义后,内存布局如下所示:

高地址 |____________| <-- STACK_TEST_P_4 (哨兵区2上边界) |____________| 哨兵区2 (Stack Guard Above) |____________| <-- STACK_TEST_P_3 (哨兵区2起始地址/栈顶之上) |____________| <-- __BOOT_STACK_ADDRESS (栈顶,初始SP) | | | 栈 | <-- 应用程序栈空间 (向下生长) | | |____________| <-- STACK_TEST_P_2 (栈底之下/哨兵区1上边界) |____________| 哨兵区1 (Stack Guard Below) |____________| <-- STACK_TEST_P_1 (哨兵区1起始地址) 低地址

STACK_TEST_P_1STACK_TEST_P_3这两个地址被导出为全局符号,使得C代码可以访问它们,从而进行初始化和测试。

踩坑记录:链接器配置是栈测试失败的首要原因。务必确保:

  1. STACK_TEST_BLOCK_SIZE是4字节对齐的。
  2. 哨兵区已被成功从RAM区域中排除(- mem:[from ...]语句)。你可以通过生成的map文件来验证,这些地址区域是否没有被分配任何变量。
  3. 栈大小(__ICFEDIT_size_cstack__)估算要充足,需考虑最坏情况下的嵌套调用、中断嵌套以及局部变量使用。太小会导致栈溢出到哨兵区,触发误报警;太大则浪费RAM。

3.2 初始化与测试流程

配置好链接器后,在软件中的操作就相对标准化了。

初始化阶段 (FS_CM4_CM7_STACK_Init):main函数开始、任何栈操作发生之前,必须调用初始化函数。它的作用就是用特定的“魔法数字”(如0x77777777)填满上下两个哨兵区。

#include "iec60730b.h" extern unsigned long STACK_TEST_P_2; extern unsigned long STACK_TEST_P_3; const unsigned long stack_test_pattern = 0x77777777; const unsigned long stack_test_block_size = 0x10; void System_Init(void) { // ... 其他硬件初始化 FS_CM4_CM7_STACK_Init(stack_test_pattern, (uint32_t)&STACK_TEST_P_2, (uint32_t)&STACK_TEST_P_3, stack_test_block_size); // ... 允许使用栈的操作 }

测试阶段 (FS_CM4_CM7_STACK_Test):在系统运行期间,周期性地调用测试函数。它会逐个字节地检查两个哨兵区,看其中的值是否还是当初写入的“魔法数字”。

  • 如果值全部匹配:返回FS_PASS,说明栈使用规整,没有越界。
  • 如果任何一个值被改变:返回FS_FAIL_STACK。这极有可能意味着发生了栈溢出(值被向下生长的栈修改)或栈下溢(值被异常向上修改,虽不常见但可能由严重错误导致)。

3.3 模式选择与高级考量

  • “魔法数字”的选择0x77777777是一个不错的选择,因为它既非全0也非全1,且是一个不常见的值,减少了被程序正常数据偶然覆盖的概率。你也可以选择其他值,但要避免使用像0x000000000xFFFFFFFF这类在未初始化内存或错误中常见的数据。
  • 测试频率:栈测试的频率需要权衡。太频繁会增加CPU开销;间隔太长则可能无法及时发现瞬时溢出(特别是由中断服务程序导致的)。通常将其置于一个周期为几十到几百毫秒的低优先级任务中。
  • 与MPU/MMU结合:在一些高端Cortex-M芯片上,可以使用内存保护单元(MPU)为栈区域设置严格的读写权限。当栈溢出试图写入保护区域时,MPU会立即触发MemManage异常,这比软件周期性检测更及时。软件栈测试可以与MPU配合,提供更深层的防御。

4. 看门狗测试:验证“终极守护者”是否可靠

看门狗(Watchdog)是嵌入式系统最后的救命稻草。它的原理很简单:系统需要定期“喂狗”,如果超过预定时间未喂食,看门狗就认为系统已“死机”,并触发复位。但这里存在一个循环论证:我们如何知道这个负责复位的看门狗本身是好的?看门狗测试就是为了解决这个“谁来监督监督者”的问题。

4.1 测试策略:主动触发与验证

库中实现的看门狗测试,其核心策略是主动进行一次可控的看门狗超时复位,并验证这次复位是否在预期的时间内发生。这需要两个独立的定时器协同工作:

  1. 被测看门狗定时器 (WDT):我们期望它超时并引发复位。
  2. 参考定时器 (如LPTMR, RTC):一个独立时钟源的定时器,用于精确测量从测试开始到看门狗复位实际发生所经过的时间。

测试分两个阶段,跨越一次系统复位:

第一阶段(复位前,由FS_WDOG_Setup_xxx函数执行):

  1. 配置好看门狗(较短的超时时间,如100ms)和参考定时器。
  2. 启动参考定时器。
  3. 停止喂狗,让程序进入一个空循环,等待看门狗超时复位。
  4. 在循环中,不断将参考定时器的当前计数值保存到一个特殊的、复位后不会丢失的备份RAM变量中。

第二阶段(看门狗复位后,由FS_WDOG_Check函数执行):

  1. 系统从看门狗复位中启动。
  2. 读取备份RAM中保存的参考定时器最终值。
  3. 将这个值与基于看门狗超时设置计算出的预期时间窗口进行比较。
    • 如果该值落在预期窗口内(例如,理论超时100ms,实测值在95ms到105ms之间),说明看门狗功能正常,计时准确。
    • 如果该值远小于预期(例如,只有10ms),说明看门狗过早复位,可能其时钟源过快或逻辑错误。
    • 如果该值远大于预期,或者备份变量损坏,说明看门狗未能按时复位,功能已失效。
  4. 此外,该函数还会检查看门狗复位计数器。如果短时间内看门狗复位次数异常过多,可能指示系统存在严重不稳定问题。

4.2 关键配置与实现细节

提供的示例代码清晰地展示了整个流程,其中有几个魔鬼细节:

#define WD_TEST_LIMIT_HIGH 3400 // 参考定时器计数值上限(对应时间上限) #define WD_TEST_LIMIT_LOW 3000 // 参考定时器计数值下限(对应时间下限) #define ENDLESS_LOOP_ENABLE 1 // 检查失败后是否进入死循环 #define WATCHDOG_RESETS_LIMIT 1000 // 允许的看门狗复位次数上限 #define WATCHDOG_TIMEOUT_VALUE 100 // 看门狗超时值(单位取决于配置) // 判断复位来源 if (RCM_SRS0_POR_MASK == (RCM_SRS0_POR_MASK & RCM_SRS0)) { // 上电复位 FS_WDOG_Setup(WATCHDOG_TEST_VARIABLES, REFRESH_INDEX); // 第一次运行,启动测试 } if (RCM_SRS0_POR_MASK != (RCM_SRS0_POR_MASK & RCM_SRS0)) { // 非上电复位(即看门狗复位) FS_WDOG_Check(WD_TEST_LIMIT_HIGH, WD_TEST_LIMIT_LOW, WATCHDOG_RESETS_LIMIT, ENDLESS_LOOP_ENABLE, WATCHDOG_TEST_VARIABLES, CLEAR_FLAG, REG_WIDE); }
  • 复位源鉴别:通过芯片的复位状态寄存器(如RCM_SRS0)区分上电复位和看门狗复位。只有看门狗复位后才执行检查阶段。
  • 备份RAM:用于存储计数值的结构体fs_wdog_test_t必须存放在一个特殊的RAM区域,该区域在非上电复位(看门狗复位、外部复位等)后内容得以保持。这通常需要在链接脚本中定义一个NO_INIT段。
  • 时钟源独立性:这是安全要求的核心。看门狗的时钟源和用于测量的参考定时器时钟源必须是相互独立的。如果它们共用同一个时钟,那么当时钟源本身发生故障(如停振)时,两个定时器会一起失效,测试将无法检测到故障。通常,看门狗使用内部低速RC振荡器(LPO),而参考定时器可以使用主时钟分频或其他独立的时钟源。
  • 超时窗口WD_TEST_LIMIT_LOWWD_TEST_LIMIT_HIGH需要根据看门狗超时设置和参考定时器的时钟频率精心计算,并留出合理的容差,以覆盖时钟精度和代码执行时间的微小偏差。

4.3 安全状态与错误处理

看门狗测试失败意味着最后的安全防线可能已崩溃,处理必须坚决:

  • FS_WDOG_Check函数检测到超时时间异常或复位次数超限时,如果ENDLESS_LOOP_ENABLE设置为1,它会直接进入一个关中断的死循环。
  • 这个死循环会导致看门狗再次超时复位。如果看门狗功能正常,系统会再次复位并重新检查。如果看门狗功能已失效,系统将永远挂在此处。
  • 这种设计强制系统在关键安全功能不确定时,无法进入正常的、可能不安全的运行模式。在实际产品中,除了死循环,可能还需要在此刻控制硬件进入一个确定的“故障安全状态”,例如关闭所有功率输出。

5. 常见问题与排查技巧实录

在实际项目中集成这些安全测试时,你几乎一定会遇到下面这些问题。这里是我踩过坑后总结的排查清单:

问题1:栈测试始终失败,返回FS_FAIL_STACK

  • 排查思路
    1. 检查链接脚本:这是最常见的原因。使用生成的map文件,确认STACK_TEST_P_1STACK_TEST_P_3地址区域确实没有被分配任何全局变量、堆或静态数据。确保排除语句(- mem:[from ...])语法正确。
    2. 检查栈大小:通过调试器或静态分析工具(如addr2line-fstack-usage编译选项)评估你的应用程序在最坏情况下的栈使用量。你可能严重低估了所需栈空间,特别是当使用了递归、大型局部数组或深度中断嵌套时。
    3. 检查初始化时机:确保FS_CM4_CM7_STACK_Init是在任何可能使用栈的操作(包括C运行时环境初始化、静态对象构造函数调用)之前被调用的。有时需要将它放在启动代码中非常靠前的位置。
    4. 检查“魔法数字”:确认初始化函数和测试函数使用的是完全相同的模式值、起始地址和块大小。

问题2:看门狗测试无法进入检查阶段,或者检查总是失败。

  • 排查思路
    1. 确认复位来源:在FS_WDOG_Check函数入口处打印或通过调试器查看复位状态寄存器的值。确认系统复位确实是由看门狗触发的,而不是其他原因(如外部复位引脚、软件复位)。
    2. 检查备份RAM:验证存放fs_wdog_test_t结构体的内存区域是否真的在非上电复位后保持了数据。可以在FS_WDOG_Setup中写入一个特殊标记,在FS_WDOG_Check中首先读取这个标记,看是否有效。
    3. 核对时钟配置:这是另一个高频故障点。仔细检查参考手册,确认看门狗时钟和LPTMR/RTC时钟是否配置为来自两个独立的时钟源(例如,WDT用LPO,LPTMR用MCGIRCLK)。检查相关时钟门控是否已使能。
    4. 校准超时窗口:首次测试时,可以将ENDLESS_LOOP_ENABLE设为0,并在检查失败时,通过调试接口输出参考定时器捕获的实际值。用这个实际值来反推和校准WD_TEST_LIMIT_LOW/HIGH的设定值。

问题3:CPU寄存器测试在特定中断服务程序(ISR)执行后失败。

  • 排查思路
    1. 检查测试时机:确保寄存器测试不是在中断上下文或临界区内执行的。某些测试(如栈指针测试)要求关中断,而其他测试如果在中断中被高优先级中断打断,也可能导致状态混乱。
    2. 检查寄存器上下文保存:在ARM Cortex-M中,进入中断时CPU会自动将部分寄存器(包括xPSR,PC,LR,R12,R3-R0)压栈。确保你的测试代码没有依赖于这些会被硬件自动修改的寄存器值。
    3. 关注BASEPRI测试:如果你的应用在ISR中动态修改了BASEPRI的值以管理中断嵌套优先级,那么在ISR退出后、主循环测试执行前,BASEPRI可能已被恢复。测试函数写入的值可能会被ISR中的操作覆盖,导致回读不一致。需要协调好ISR中的优先级管理与测试时机。

问题4:安全测试增加了CPU负载,影响了系统实时性。

  • 优化策略
    1. 分时执行:不必在每个循环中都执行全部测试。可以将不同的测试分摊到不同的时间片。例如,每10ms执行一次栈测试,每100ms执行一次CPU寄存器测试,每10秒执行一次看门狗完整测试。
    2. 优化测试模式:对于寄存器测试,如果确认某些寄存器在应用运行中不会被修改,可以考虑降低其测试频率。
    3. 使用硬件特性:如果芯片支持,用MPU进行栈保护比软件测试更高效。一些芯片还有硬件自检(BIST)模块,可以分担CPU的测试负担。
    4. 性能剖析:利用文档中给出的周期数,精确计算所有安全测试在最坏情况下的总执行时间,确保其不超过分配给后台安全任务的时间预算。

将这些测试无缝、可靠地集成到你的嵌入式系统中,就像为你的代码搭建了一个持续运行的“健康监测系统”。它不会让代码本身变得更正确,但能在硬件偶尔“犯错”时,给你一个检测、处理和恢复的机会,而这正是功能安全的核心价值所在。

http://www.jsqmd.com/news/1029569/

相关文章:

  • 2026 南京闲置奢品回收 TOP5 榜单,多门店报价横向对比实测 - 讯息早知道
  • Java计算机毕设之基于 SpringBoot 的设施番茄水肥一体化精准管理系统设计 现代农业视角下番茄水肥灌溉智能管控系统设计与实现(完整前后端代码+说明文档+LW,调试定制等)
  • 欧富洛宋式美学北美黑胡桃木实木家具:FAS级全实木榫卯工艺诠释东方极简雅致生活 - 优选案例分享
  • WeChatExporter终极指南:免费永久保存微信聊天记录的完整解决方案
  • 上海全屋定制优选推荐:上海宝泉建材兔宝宝全屋定制一站式解决方案 - 品牌推荐官
  • 一文分清五轴雕刻机专业与杂牌差距,工坊购机避坑指南
  • 2026广州荔湾区首饰回收门店,手链耳饰无隐形收费 - 逸程
  • 学员作品人气评选怎么弄?微信线上投票创建完整教程 - 微信投票小程序
  • LPC55Sxx IEC60730B安全库实战:从硬件连接到CRC校验的嵌入式功能安全集成指南
  • 广东成考报名进行中,官方助学点筛选标准完整指南! - 一直爱学习的小花猫
  • Mac 移动硬盘无法新建文件夹?别急,3 招搞定它 - 雨林谷
  • 基于Gemini大模型的安全PoC脚本自动化生成实战指南
  • ZigBee PRO协议栈实战:从API调用到网络参数调优的深度解析
  • 北京保洁服务推荐:百发伟业专业石材翻新、地毯清洗及全品类保洁工程 - 品牌推荐官
  • 2026-05-16 星期二 【ng】 心态崩 操作变形
  • 六层电路板打样怎么选?老电子工程师的真实经验
  • Adobe-GenP 3.0终极指南:5分钟解锁Adobe全系列软件完整功能
  • 泸州黄金铂金K金钻石回收哪里靠谱?本地真实测评榜单与避坑指南 - 热点速览
  • 2026桥西区废旧金属回收公司 实测测评 - LYL仔仔
  • 2026长沙高端系统门窗定制:从隔音隔热到全屋改善的深度选型指南 - 优质企业观察收录
  • 江苏信益鑫照明科技:工业照明灯具全系供应,提供一站式照明解决方案 - 品牌推荐官
  • 2026 年 6 月最新 | 台车式退火炉 / 回火炉 / 台车炉厂家实测排名权威榜单推荐,避开劣质厂家采购干货大全 - 商业新知
  • Qt容器删除操作避坑指南:从QList到QHash的性能陷阱与最佳实践
  • 2026甄选:苏州电子设备回收专业公司,工厂级报废处置与环保变现方案 - 品牌发掘
  • 2026年纤维素厂家实力推荐:晋州市兴东建材科技多品类纤维素专业供应 - 品牌推荐官
  • 2026保姆级指南:手机免费制作证件照方法,好用工具全推荐,一看就会 - 软件小管家
  • 中国科学院大学考研辅导班推荐榜单:含报班选型指南与实力评测 - michalwang
  • 【开源推荐】obsidian-wiki——给 AI Agent 造一颗会成长的数字大脑
  • 52|提示注入:为什么“文档内容”能劫持你的 Agent
  • 2026 赣州防水补漏靠谱商家推荐排行榜:全屋渗漏综合治理,卫生间免砸砖防水、屋顶飘窗、阳台外墙、地下室漏水检测修复、瓷砖空鼓翻新测评 - 泛家庭维修