
1. 项目概述为什么要动Flash起始地址做嵌入式开发的朋友迟早会遇到这么一件事手里的STM32F103跑得好好的突然有一天要做Bootloader要把固件分成“引导程序”和“应用程序”两份或者要做OTA远程升级这时候就不得不面对一个绕不开的问题——App程序的起始地址不能再从0x08000000开始了得往后挪。这个“挪”的过程就是本文要讲的Flash起始地址调整。表面上看只是改一个数字实际牵扯到链接脚本、启动文件、向量表偏移、烧录器的配置一环扣一环。任何一个环节漏了程序要么烧不进去要么烧进去直接跑飞要么中断全部失灵一进中断就死机。我最初用的是Keil后来整个工具链迁到CLion CubeMX CMake这套组合之后发现网上关于“在CLion环境下调整Flash起始地址”的完整资料并不多大部分教程还在讲Keil里那个IROM1起始地址怎么改。Keil改法确实简单点两下就行但CMake环境下要靠改链接脚本.ld文件实现原理相通操作完全不同。本文就从实际项目出发把F103在CubeMX CLion环境下调整Flash起始地址的完整链路拆开讲清楚原理是什么、哪些文件要改、CMake怎么配置、改完怎么验证。我用的是STM32F103C8T6这颗芯片做演示但方法对F103全系列都适用。适合正在做Bootloader/App分离、OTA升级或者单纯想把整个工具链从Keil迁到CLion的朋友做参考。2. 整体设计思路先理解Flash布局再做改动2.1 一个典型场景Bootloader App双分区假设我们做一个带Bootloader的固件方案Flash分配大概是这样的0x08000000 ~ 0x08003FFF16KBBootloader程序区0x08004000 ~ 0x0800FFFF48KBApp程序区起始地址即0x08004000最后还有一小块做参数存储区放一些校准参数、升级标志等很多IAP方案会把Bootloader控制在16KB甚至8KB以内剩下的大头全部留给App。之所以要这么分区是因为Bootloader和App是两套独立编译的固件各自有完整的启动文件和中断向量表。芯片上电先跑BootloaderBootloader再跳转到App的起始地址执行。这里的关键点在于App编译时所有绝对地址引用、中断向量表位置、链接时的起始地址都必须按“从0x08004000开始”这个前提来做。如果App仍然按默认的0x08000000编译跳转过去直接崩溃因为向量表位置对不上代码段位置也全乱了。2.2 CubeMX CLion组合的优势这套组合最大的优势是工程文件全文本化。CubeMX生成的.ioc文件、CLion的CMakeLists.txt文件、GCC的链接脚本全部可以用git做版本管理diff起来清清楚楚。相比之下Keil的.uvprojx虽然也是XML文本但每次在IDE里点一下界面设置生成的改动一堆review起来很头疼。CubeMX负责外设初始化和时钟树配置这部分它做得足够好。CLion负责代码编辑、编译、调试它的代码分析、重构、git集成比Keil舒服太多。中间由CMake arm-none-eabi-gcc这套工具链衔接。这套组合这几年在嵌入式圈子里越来越流行相关的坑也基本被踩得差不多了整体是可用的。2.3 方案选型的三个替代方案对比在动手之前先看看有没有其他路子免得白折腾方案实现方式优点缺点Keil直接改IROM1地址IDE设置里改起始地址和大小操作最简单适合单次开发绑定Keil生态多人协作不如文本化STM32CubeIDE同样改链接脚本和CubeMX同源生态导入方便Eclipse系界面老旧内存占用高CLion CMake改.ld手动改链接脚本全文本化可版本控制编译快需要理解链接脚本语法有一定门槛我推荐第四种组合——CLion CMake。理由就一个字可控。所有配置都在文本里出了问题能查到根因而不是在IDE的某个隐藏配置项里瞎找。3. 核心原理链接脚本、向量表和启动流程3.1 链接脚本.ld文件怎么决定程序的位置STM32F103工程编译时GCC需要一份链接脚本来告诉链接器代码该从哪个地址放起RAM从哪个地址用起各个段怎么排布。以STM32F103C8T6为例CubeMX生成的默认.ld文件核心部分长这样/* 默认配置Flash 64KBRAM 20KB起始地址都是芯片默认值 */ MEMORY { RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K }ORIGIN是起始地址LENGTH是长度。链接器看到这两个参数就会把.text代码段、.rodata只读数据段放到FLASH区域从0x08000000开始布局把.data初始化的全局变量、.bss未初始化变量、堆栈放到RAM区域。如果我们想从0x08004000开始放App核心改动就是这一处FLASH (rx) : ORIGIN 0x08004000, LENGTH 48K这样链接器就会把所有代码段放在0x08004000之后的48KB范围内。RAM一般不动0x20000000 20K保持默认即可。3.2 向量表偏移中断能正常工作的前提光改Flash分配还不够必须把中断向量表也挪过去。STM32的向量表默认在0x08000000即Flash起始位置。芯片上电后CPU从0x08000000处读取初始堆栈指针_estack和复位中断处理函数地址Reset_Handler这是设计时写死在芯片内部的。App跑在0x08004000以后中断向量表也自然要跟着挪到0x08004000。这一步不能指望链接器自动完成必须手动设置方法一修改system_stm32f1xx.c里的VECT_TAB_OFFSET宏定义方法二在main()早期调用SCB-VTOR寄存器赋值F1系列和F4系列有所区别。F4的SystemInit()会自动调用向量表偏移函数F1则需要手动作这一步因为它默认的VECT_TAB_OFFSET是0。CubeMX生成的代码中system_stm32f1xx.c文件里有这行#define VECT_TAB_OFFSET 0x0我们要把它改成偏移量。0x08004000 - 0x08000000 0x4000也就是16KB#define VECT_TAB_OFFSET 0x4000同时确认下面这行没有被注释掉SCB-VTOR FLASH_BASE | VECT_TAB_OFFSET;这个宏定义和赋值的关系需要解释一下。FLASH_BASE在stm32f103xb.h里定义为0x08000000SCB-VTOR是ARM Cortex-M3核内的“向量表偏移寄存器”CPU从IDLE状态被中断唤醒时会读取这个寄存器的值找到对应向量表的位置。如果这个值是0x08000000而向量表实际在0x08004000任何中断触发都会让CPU从错误的地址取中断处理函数指针结果就是进中断就进HardFault。3.3 启动文件需要动吗理论上启动文件startup_stm32f103xb.s中的Reset_Handler负责搬运.data、清零.bss、调用SystemInit()和main()这些操作基于链接器生成的符号链接脚本改对了启动文件不需要手动改。但从用户实际反馈和我的经验来看有一个容易忽略的坑默认的startup_stm32f103xb.s中向量表第一项是_estack栈顶地址这个符号在链接脚本里定义。当你修改Flash区域时如果不小心动到了_estack的定义位置启动文件会出问题。实践中的建议是启动文件不用改但你要理解它的原理这样出了问题才知道去哪里排查。4. 实操步骤CubeMX工程生成与CLion环境搭建4.1 用CubeMX生成基础工程如果你已经用CubeMX建好工程了这部分可以直接跳过。如果还没建按下面的步骤来打开CubeMX选择芯片STM32F103C8Tx配置时钟树HSE外部晶振8MHzPLL倍频到72MHzAPB1为36MHzAPB2为72MHz开启需要的调试接口强烈建议打开Serial Wire否则用ST-Link烧录一次之后第二次可能无法连接配置外设比如USART1PA9/PA10做调试串口波特率115200Project Manager页面设置工程名和路径注意Toolchain/IDE这一栏选择CMake关键点在这里。CubeMX的Toolchain下拉菜单里有MDK-ARM、STM32CubeIDE、EWARM、Makefile等选项。不同版本里CMake选项的位置不一样较新的CubeMX版本直接就有CMake选项如果没有也可以先选Makefile后续在CLion里导入时CLion会自动识别。但为了省事建议直接选CMake。生成之后工程目录下会出现Core/启动文件、main.c、中断处理文件Drivers/HAL库文件CMakeLists.txt整个工程的编译配置*.iocCubeMX的配置源文件以后要改外设配置就打开这个文件4.2 CLion和工具链准备CLion本质是一个通用的IDE底层编译还是靠GCC工具链。嵌入式开发需要三样东西arm-none-eabi-gcc工具链ARM官方提供的交叉编译器OpenOCD或者ST-Link驱动负责把固件烧到芯片里并支持在线调试CMake和Make工具CLion自带了CMake但为了可靠性建议系统单独安装工具链安装完成后在CLion的Settings - Build, Execution, Deployment - Toolchains里添加一个工具链C Compiler选择arm-none-eabi-gccDebugger选择arm-none-eabi-gdb。然后把OpenOCD的安装路径填到Embedded Development选项卡下面。4.3 编译第一个默认工程在还没有修改Flash地址之前先编译一遍基础工程确保工具链正常工作。这一步非常重要能排除“环境问题”和“代码问题”的干扰。如果这一步编译失败工具链路径、CMake版本、编译器版本都要重新检查。CLion里直接点击右上角的构建按钮即可。编译产物在cmake-build-debug目录下是一个.elf文件。5. 核心环节实现修改链接脚本和CMake配置5.1 链接脚本修改实操找到工程里的STM32F103C8TX_FLASH.ld文件不同CubeMX版本文件名可能略有差异比如stm32f103c8tx_flash.ld。打开后把FLASH区域这一行修改如下/* 原配置 */ FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K /* 新配置起始地址偏移16KB可用大小48KB */ FLASH (rx) : ORIGIN 0x08004000, LENGTH 48K如果Bootloader计划占用32KBApp起始地址就是0x08008000可用大小32KBFLASH (rx) : ORIGIN 0x08008000, LENGTH 32K这里的LENGTH取决于你希望给App分配多大的空间。C8T6的Flash总大小是64KBBootloader占了0x08000000 ~ 0x08003FFF这16KB剩下自然就是48KB。Bootloader占得越多App的空间就越小编译时如果App固件大小超过这个长度链接器会直接报错报错信息类似region FLASH overflowed by xxx bytes这个报错是好事能提前暴露空间不足问题。还有一种情况你用的是F103C8的“非官方”128KB Flash版本也就是市面上常说的“C8T6其实是CB的die”那种。这种场景下很多开发者会把LENGTH设成128K把App放更大的空间。这里我不建议在C8T6上这么做因为不是所有批次都保证有128KB固件写超出64KB范围之外一旦芯片不支持程序就跑飞了。稳妥的做法是确认MCU型号按照官方规格来分配空间。5.2 CMakeLists.txt的完整配置解析CubeMX生成的CMakeLists.txt核心结构如下新版本CubeMX生成的略有简化但关键元素一致cmake_minimum_required(VERSION 3.16) project(flash_offset_demo C ASM) set(CMAKE_C_STANDARD 11) # 设置编译工具链和硬件浮点选项F1没有FPU不涉及浮点单元配置 add_compile_definitions( STM32F103xB USE_HAL_DRIVER USE_FULL_LL_DRIVER ) add_compile_options( -mcpucortex-m3 -mthumb -ffunction-sections -fdata-sections ) # 链接选项修改Flash起始地址后的关键 add_link_options( -mcpucortex-m3 -mthumb -T ${CMAKE_SOURCE_DIR}/STM32F103C8TX_FLASH.ld -Wl,--gc-sections ) # 源文件收集 file(GLOB_RECURSE SOURCES Core/*.c Core/*.s Drivers/*.c ) add_executable(${PROJECT_NAME}.elf ${SOURCES}) # 生成 hex 和 bin 文件 add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O ihex ${PROJECT_NAME}.elf ${PROJECT_NAME}.hex COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin )几个关键点详细展开第一点-T参数必须传链接脚本路径。如果链接脚本不在工程根目录路径写错会导致链接失败。CubeMX生成的CMakeLists里通常已经包含了-T参数部分老版本没有如果没有需要手动加。路径有两种写法相对路径${CMAKE_SOURCE_DIR}/链接脚本文件名或者绝对路径。建议用${CMAKE_SOURCE_DIR}引用这样整个工程拷到任何一台机器上只要路径结构不变就能正常编译。第二点-ffunction-sections和-fdata-sections配合--gc-sections。这三个选项的作用是让链接器删除未被引用的函数和数据段减小固件体积。对F103这种Flash只有64KB的芯片来说能省一点是一点。这三个选项也不是必须的但不加的话固件会明显偏大对于本就不宽的Flash空间很不友好。第三点CubeMX生成的CMakeLists中target_link_libraries通常为空但编译时仍然能正常链接因为所有源码都直接进了add_executable。如果你以后要添加外部的静态库或者自己写的库文件需要在这个函数的PRIVATE后面追加。第四点关于CubeMX新老版本CMakeLists的差异。老版本比如6.x早期生成的CMakeLists里会带一个target_include_directories列出所有头文件路径。新版本改用了target_include_directories里引用${CMAKE_CURRENT_SOURCE_DIR}整体差异不大看懂源码就行。5.3 修改向量表偏移配置链接脚本和CMake改完第三步是改向量表偏移。打开Core/Src/system_stm32f1xx.c找到#define VECT_TAB_OFFSET 0x0修改为#define VECT_TAB_OFFSET 0x4000保存后检查SystemInit()函数末尾是否存在这段代码#ifdef VECT_TAB_SRAM SCB-VTOR SRAM_BASE | VECT_TAB_OFFSET; #else SCB-VTOR FLASH_BASE | VECT_TAB_OFFSET; #endif如果你的CubeMX生成的代码里没有这个宏定义某些精简版本可能没有可以自己在main()函数里的HAL_Init()之后手动加一行int main(void) { HAL_Init(); /* 手动设置向量表偏移必须在任何中断使能之前完成 */ SCB-VTOR 0x08004000; SystemClock_Config(); /* 其他初始化 */ while (1) { } }这一行必须在使能任何外设中断之前执行否则中断一旦触发CPU从错误的向量表地址取函数指针直接HardFault。5.4 烧录配置CLion的烧录调试默认走OpenOCD需要一个.cfg配置文件。如果你之前没配过CLion的Run/Debug Configurations里新建OpenOCD Download Run配置文件选ST-Link的source [find interface/stlink.cfg] source [find target/stm32f1x.cfg]这里要注意一个细节stm32f1x.cfg里默认的flash bank配置是针对整个Flash的。如果你的App直接通过OpenOCD烧录烧录地址会从链接脚本计算的地址开始不会覆盖Bootloader区域这一点是安全的。但如果需要整片擦除或者单独擦Bootloader区域需要执行flash erase_address 0x08000000 0x4000或者制定bank范围避免误操作把Bootloader清掉。5.5 编译验证与烧录验证改完以上三处链接脚本、向量表偏移、确认CMake配置重新编译。查看编译日志重点确认链接时用的链接脚本路径正确没有报undefined symbol或者region FLASH overflowed之类的错误。用OpenOCD烧录后按复位键。如果一切正常程序能跑起来LED闪烁、串口输出这些功能全部正常。怎么判断程序确实跑在0x08004000最简单的方法在调试器里查看PC寄存器或者反汇编窗口确认第一条指令的地址是0x08004000附近。CLion调试模式下程序暂停时看Registers窗口的pc值就能直观确认。6. 完整可复制的配置模板为了不让大家照着文章改的时候漏东漏西我把自己验证过的整套关键文件内容整理成模板可以直接复制套用。6.1 链接脚本核心段STM32F103C8T6Bootloader占16KB/* 入口点 */ ENTRY(Reset_Handler) /* 栈顶地址 */ _estack ORIGIN(RAM) LENGTH(RAM); /* 内存布局 */ MEMORY { RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K FLASH (rx) : ORIGIN 0x08004000, LENGTH 48K } /* 输出段定义无需改动沿用模板 */这里的_estack是栈顶地址定义为RAM的最高地址。ARM Cortex-M3的栈是向下增长的所以栈顶设在RAM末尾栈向下生长不会越界。不同型号的F103的RAM大小不同C8T6是20KBRCT6是48KB用哪个芯片就按哪个芯片的规格来。6.2 CMakeLists.txt完整模板cmake_minimum_required(VERSION 3.16) project(stm32f103_flash_offset C ASM) set(CMAKE_C_STANDARD 11) set(CMAKE_C_FLAGS_DEBUG -g3 -O0) set(CMAKE_C_FLAGS_RELEASE -O2) add_compile_definitions( STM32F103xB USE_HAL_DRIVER ) add_compile_options( -mcpucortex-m3 -mthumb -ffunction-sections -fdata-sections ) # 注意链接脚本路径根据自己的工程目录调整 set(LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/STM32F103C8TX_FLASH.ld) add_link_options( -mcpucortex-m3 -mthumb -T ${LINKER_SCRIPT} -Wl,--gc-sections -Wl,-Map${CMAKE_PROJECT_NAME}.map ) file(GLOB_RECURSE SOURCES Core/*.c Core/*.s Drivers/*.c ) add_executable(${PROJECT_NAME}.elf ${SOURCES}) add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O ihex ${PROJECT_NAME}.elf ${PROJECT_NAME}.hex COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin )加-Wl,-Map参数会生成映射文件里面能看到每个段被放到哪个地址检查FLASH段的起始地址是否为0x08004000非常直观。建议保留。6.3 向量表偏移调整后的SystemInit片段/* 修改后的宏定义 */ #define VECT_TAB_OFFSET 0x4000 void SystemInit(void) { /* 重置RCC寄存器为默认值 */ RCC-CR | (uint32_t)0x00000001; /* 其余RCC复位序列 */ /* 设置中断向量表偏移 */ #ifdef VECT_TAB_SRAM SCB-VTOR SRAM_BASE | VECT_TAB_OFFSET; #else SCB-VTOR FLASH_BASE | VECT_TAB_OFFSET; #endif }7. 常见问题排查与避坑笔记这个问题我在网上见了无数人问也踩过不少专门整理成速查表方便大家直接对照排查。7.1 问题速查表现象可能原因解决办法编译报错region FLASH overflowedApp固件大小超过链接脚本中分配的FLASH LENGTH扩大App分区缩小Bootloader或优化代码减小固件体积烧录失败Error: flash download failed - target dll has been cancelled接线问题、芯片锁死、烧录器驱动问题检查ST-Link接线和驱动用CubeProgrammer连接必要时整片擦除程序烧进去但灯不闪/无输出向量表偏移没设置或者设置的偏移量和链接脚本不一致确认VECT_TAB_OFFSET的值等于(App起始地址 - 0x08000000)程序能启动一进中断就死机向量表偏移设置晚了已经有过中断触发把SCB-VTOR赋值放到HAL_Init()之后、使能中断之前跳转到App后程序跑飞Bootloader跳转方式不对或者App起始地址处不是有效的栈指针检查跳转地址是否为0x08004000 4处取复位地址用函数指针跳转串口输出乱码波特率配置、时钟配置或串口引脚配置问题对照CubeMX配置检查时钟树和USART参数7.2 关于flash download failed - target dll has been cancelled这个报错在Stack Overflow上和国内论坛上都非常常见。说几个我实测过的排查方向首先确认ST-Link和MCU的接线SWDIO、SWCLK、GND三根线是最低要求最好加上VCC和NRST。然后检查MCU供电是否正常F103的PA11和PA12在某些板卡上有特殊电平和USB功能冲突会导致调试接口异常这也是搜索热词里有人问“stm32f103 pa11 bug”的原因。如果复位不住按一下复位键再烧录。如果以上都不行用STM32CubeProgrammer连接一下选择“Connect Under Reset”模式能连上就直接擦除整片再重新烧录。7.3 Bootloader跳转App的一个经典坑很多人Bootloader和App分开都编译过了App也能独立跑但是从Bootloader跳过去就死机。这里有一个经典原因跳转时外设的中断没有关闭。Bootloader初始化了串口、定时器等外设这些外设的中断在跳转前是使能状态。跳转之后App的SystemInit()会重新设置时钟和向量表但此时如果还有挂起的中断没有处理CPU会尝试查向量表而App的向量表偏移如果设置稍晚就会出现问题。正确的跳转代码应该是/* 跳转前关闭所有中断 */ __disable_irq(); /* 确认App起始地址合法 */ uint32_t app_addr 0x08004000; uint32_t app_sp *(volatile uint32_t *)app_addr; uint32_t app_pc *(volatile uint32_t *)(app_addr 4); /* 检查栈顶地址是否在RAM范围内 */ if ((app_sp 0xFFF00000) ! 0x20000000) { return; /* 栈顶地址异常不跳转 */ } /* 设置MSP跳转到App */ __set_MSP(app_sp); /* 重新使能中断 */ __enable_irq(); /* 跳转 */ void (*jump_to_app)(void) (void (*)(void))app_pc; jump_to_app();注意函数指针跳转会直接跳转但仍然要保持状态一致。官方推荐的HAL库中也有类似的跳转示例可以参考。7.4 为什么烧录成功但复位后程序不运行还有一个容易被忽视的点链接脚本的FLASH区域ORIGIN改了但烧录器的算法默认从0x08000000开始烧录。如果你通过OpenOCD烧录和调试烧录地址取决于.elf文件内的段地址信息一般没问题。但如果用某些第三方工具直接烧bin文件需要手动设置烧录起始地址为0x08004000否则固件被烧到0x08000000而0x08000000正是Bootloader的位置相当于覆盖了Bootloader程序自然跑的不是你预期的逻辑。7.5 关于CubeMX重新生成代码时配置被覆盖这是用CubeMX开发时比较困扰的问题。.ioc文件是CubeMX的工作源每次在CubeMX里打开它并重新生成代码Core/Src下的文件会被重新生成你在里面手动加的代码如果没有放在用户代码区/* USER CODE BEGIN */和/* USER CODE END */之间会被直接覆盖掉。解决方案有两个一是修改代码时尽量放在CubeMX的用户代码区内二是链接脚本和CMakeLists这类文件CubeMX重新生成时只会生成缺失的部分不会覆盖你手动改过的内容所以放心改.ld文件。但对于main.c里加的SCB-VTOR赋值建议放在/* USER CODE BEGIN 2 */区域防止生成代码时被清除。8. 实操心得与扩展方向这套改法我已经在两个量产项目的Bootloader App方案里应用过了整个过程最花时间的其实不是改配置本身而是排查“以为改好了、实际没改对”的过程。比如向量表偏移的值和链接脚本的偏移不一致、不同工程师改的配置各自为政、CMake缓存没刷新导致链接的还是旧脚本这些问题都真实遇到过。一个特别值得说的经验是在做Bootloader App方案时建议把两段固件的编译配置统一管理。比如用脚本自动生成两个不同偏移的链接脚本避免手动改错或者把Bootloader和App放在同一个代码仓库里的不同目录用同一套CMake组织通过编译选项切换目标。这样可以减少很多低级错误。链接脚本里还有个常用技巧在MEMORY之外定义自定义符号让App代码能知道自己的Flash起始地址和版本信息。比如/* 自定义符号 */ _app_flash_start ORIGIN(FLASH); _app_flash_end ORIGIN(FLASH) LENGTH(FLASH);固件代码里用extern声明后即可引用能让OTA升级或者自校验逻辑拿到精确的分区边界比在代码里写死数字要可靠得多。如果后续要做OTA升级有两个方向值得研究。第一是固件合并Bootloader和App分开编译得到两个bin用Python脚本把两份bin按地址合并成一个完整的烧录文件产线烧录时一步到位。第二是升级失败回滚App分区之后可以再预留一个备份分区升级新固件时先写备份区校验通过后再切换失败则从备份区恢复这需要Flash分区方案再复杂一点点但逻辑是同一套。我在实际项目中最喜欢的一个操作是编译完以后用objdump看一眼各段地址arm-none-eabi-objdump -h cmake-build-debug/flash_offset_demo.elf输出里能清晰看到.text段的起始地址万一链接脚本忘了改或者改错了这一眼就能看出来比在代码里打日志定位快得多。这个习惯建议每个做嵌入式开发的朋友都养成。写到这里核心内容基本都覆盖了。F103虽然是一颗老芯片但用CLion CMake这套现代工具链做开发体验一点都不老。调整Flash起始地址这件事理解了链接脚本和向量表的原理之后换到F4、F7、H7系列也就只是改几个数值的问题方法论是通用的。