通过篡改特定代码数据修复单片机BUG的方法

一、前言      在嵌入式产品开发中,难以避免地会因为各种原因导致最后出货的产品存在各种各样的bug,通常会给产品进行固件升级来解决问题。记得之前在公司维护一款ble产品的时候,由于前期平台预研不足,ota参数设置不当,导致少数产品出现不能ota的情况,经过分析只需改变代码中的某个参数数值即可,但是产品在用户手里,ota是唯一能更新代码的方式,只能给用户重发产品。后来在想,是否可以提前做好一个接口,支持动态地传输少量代码到产品中临时运行,通过修改特定位置的flash代码数据来修复产品的棘手bug?多留一个后门,有时候令产品出棘手问题的往往是那么一两行代码或者几个初始化的参数不对,那么这种方法也可以应应急,虽然操作比较骚。
二、创建演示工程       本文以stm32f103c8t6单片机为例创建演示工程,分为app和bootloader两个工程。即将mcu的flash分为“app”和“bootloader”两个区域, bootloader放在0x8000000为起始的24kb区域内,app放在0x8006000为起始的后续区域。bootloader完成对app的flash数据修改。
1、app工程
注意app的工程需要在keil上修改rom起始地址。
还要在app代码的开头设置向量偏移(调用一行代码):
nvic_setvectortable(nvic_vecttab_flash, 0x6000);
app工程的逻辑为:先顺序执行3个不同速度的led闪灯过程(20ms、200ms、500ms、切换亮灭),最后进入到一个循环状态每秒切换一次led的状态闪烁。代码如下:
void init_led(void) {     gpio_inittypedef gpio_initstructure;     rcc_apb2periphclockcmd(rcc_apb2periph_gpiob, enable);     gpio_initstructure.gpio_pin = gpio_pin_all;      gpio_initstructure.gpio_mode = gpio_mode_out_pp;          gpio_initstructure.gpio_speed = gpio_speed_50mhz;          gpio_init(gpiob, &gpio_initstructure);         gpio_resetbits(gpiob, gpio_pin_10);     gpio_setbits(gpiob, gpio_pin_10);  } void led_blings_1(void) {     uint32_t i;     for (i = 0; i < 10; i++)     {         gpio_setbits(gpiob, gpio_pin_10);          delay_ms(20);           gpio_resetbits(gpiob, gpio_pin_10);          delay_ms(20);     } } void led_blings_2(void) {     uint32_t i;     for (i = 0; i < 10; i++)     {         gpio_setbits(gpiob, gpio_pin_10);          delay_ms(200);           gpio_resetbits(gpiob, gpio_pin_10);          delay_ms(200);     } } void led_blings_3(void) {     uint32_t i;     for (i = 0; i < 10; i++)     {         gpio_setbits(gpiob, gpio_pin_10);          delay_ms(500);           gpio_resetbits(gpiob, gpio_pin_10);          delay_ms(500);     } } int main() {     nvic_setvectortable(nvic_vecttab_flash, 0x6000);     systick_init(72);     init_led();     led_blings_1();     led_blings_2();     led_blings_3();     while (1)     {         gpio_setbits(gpiob, gpio_pin_10);          delay_ms(1000);           gpio_resetbits(gpiob, gpio_pin_10);          delay_ms(1000);     } }
为了分析汇编和查看bin文件数据,我们需要在keil中添加两条命令,分别生成.dis反汇编和.bin的代码文件。(具体的目录情况依葫芦画瓢)
fromelf --text -a -c --output=all.dis objtemplate.axf fromelf --bin --output=test.bin objtemplate.axf
先将app的代码烧写进单片机,注意烧写设置里面选择“erase sectors”只擦除需要烧写的地方。
2、bootloader工程 在bootloader中分为两部分,不变的代码部分和变动的代码部分(error_process函数)。初次编译的时候error_process写为空函数,当我们有需求对app进行修改的时候,我们重新编译工程对error_process函数进行填充。为了重新编译工程的时候不影响之前函数的链接地址,特意将error_process函数放到代码区的最后0x8000800地址处,理由是原来工程大小是1.51kb,擦除页大小是2kb,所以需要2kb对齐,对齐处的地址就选择0x8000800为起始。代码如下: #define flash_page_size 2048 #define error_process_code_addr 0x8000800 void error_process(void) __attribute__((section(.arm.__at_0x8000800))); void init_led(void) {     gpio_inittypedef gpio_initstructure;     rcc_apb2periphclockcmd(rcc_apb2periph_gpiob, enable);     gpio_initstructure.gpio_pin = gpio_pin_all;      gpio_initstructure.gpio_mode = gpio_mode_out_pp;          gpio_initstructure.gpio_speed = gpio_speed_50mhz;          gpio_init(gpiob, &gpio_initstructure);         gpio_resetbits(gpiob, gpio_pin_10);     gpio_setbits(gpiob, gpio_pin_10);  } uint32_t pagebuf[flash_page_size / 4]; void error_process(void) { } void eraseerrorprocesscode(void) {     flash_unlock();     flash_clearflag(flash_flag_bsy | flash_flag_eop |                      flash_flag_pgerr | flash_flag_wrprterr);     flash_erasepage(error_process_code_addr);     flash_lock(); } void(*boot_jump2app)(); void boot_loadapp(uint32_t addr) {     uint8_t i;     if (((*(vu32*)addr) & 0x2ffe0000) == 0x20000000)         {         boot_jump2app = (void(*)())*(vu32*)(addr + 4);               __set_msp(*(vu32*)addr);         for (i = 0; i icer[i] = 0xffffffff;              nvic->icpr[i] = 0xffffffff;          }         boot_jump2app();                 while (1);     } } int main() {     uint32_t flag;     systick_init(72);     flag = *((uint32_t *)error_process_code_addr);     if ((flag != 0xffffffff) && (flag != 0))     {         init_led();         gpio_resetbits(gpiob, gpio_pin_10);          delay_ms(1000);         delay_ms(1000);         error_process();         eraseerrorprocesscode();     }     boot_loadapp(0x8006000);     while (1); }
一进main函数就读取0x8000800地址处的32位数据,如果不是全f或者全0那么这个地方是有函数体存在需要执行的,那么将led亮起2秒钟代表bootloader识别到有处理程序需要执行(当然这里还需要加一些error_process代码数据是否完整之类的判断机制,这里演示先略去)。执行完处理程序后将处理程序擦除(数据变为全f),避免以后每次上电都重复擦写flash。
error_process函数代码的数据由产品正常使用期间通过数据接口传入直接写入到0x8000800处(这部分的demo略去),编译后查看生成的bin文件将error_process部分的代码截取出来传输到flash地址0x8000800处。bootloader的代码烧写进单片机时注意烧写设置里面选择“erase sectors”只擦除需要烧写的地方。keil设置里rom地址改回0x08000000。  三、修改app的特定参数       在app的工程中以“led_blings_1”函数为例,反汇编如下:
    $t     i.led_blings_1     led_blings_1         0x08006558:    b510        ..      push     {r4,lr}         0x0800655a:    2400        .$      movs     r4,#0         0x0800655c:    e010        ..      b        0x8006580 ; led_blings_1 + 40         0x0800655e:    f44f6180    o..a    mov      r1,#0x400         0x08006562:    4809        .h      ldr      r0,[pc,#36] ; [0x8006588] = 0x40010c00         0x08006564:    f7fffea2    ....    bl       gpio_setbits ; 0x80062ac         0x08006568:    2014        .       movs     r0,#0x14         0x0800656a:    f7ffffaf    ....    bl       delay_ms ; 0x80064cc         0x0800656e:    f44f6180    o..a    mov      r1,#0x400         0x08006572:    4805        .h      ldr      r0,[pc,#20] ; [0x8006588] = 0x40010c00         0x08006574:    f7fffe98    ....    bl       gpio_resetbits ; 0x80062a8         0x08006578:    2014        .       movs     r0,#0x14         0x0800657a:    f7ffffa7    ....    bl       delay_ms ; 0x80064cc         0x0800657e:    1c64        d.      adds     r4,r4,#1         0x08006580:    2c0a        .,      cmp      r4,#0xa         0x08006582:    d3ec        ..      bcc      0x800655e ; led_blings_1 + 6         0x08006584:    bd10        ..      pop      {r4,pc}     $d         0x08006586:    0000        ..      dcw    0         0x08006588:    40010c00    ...@    dcd    1073810432
由于led是20ms交替亮灭一次,如果我们觉得这个参数有问题想改成100ms,从汇编上来说就是要改变两行代码:     0x08006568:    2014        .       movs     r0,#0x14     0x08006578:    2014        .       movs     r0,#0x14     改为     0x08006568:    2064        2       movs     r0,#0x64     0x08006578:    2064        2       movs     r0,#0x64
bootloader工程中error_process的函数实现如下:
void error_process(void) {     #define modify_func_addr_start 0x08006558     uint32_t alignpageaddr = modify_func_addr_start / flash_page_size * flash_page_size;     uint32_t cnt, i;     // 1. copy old code     memcpy(pagebuf, (void *)alignpageaddr, flash_page_size);     // 2. change code.         //由于flash操作2kb页的特性,0x08006558不满2kb,因此偏移为0x558,0x558/4=342     pagebuf[90 + 256] = (pagebuf[90 + 256] & 0xffff0000) | 0x2064;     pagebuf[94 + 256] = (pagebuf[94 + 256] & 0xffff0000) | 0x2064;     // 3. erase old code, copy new code.     flash_unlock();     flash_clearflag(flash_flag_bsy | flash_flag_eop |                      flash_flag_pgerr | flash_flag_wrprterr);     flash_erasepage(alignpageaddr);     cnt = flash_page_size / 4;     for (i = 0; i < cnt; i++)     {         flash_programword(alignpageaddr + i * 4, pagebuf[i]);     }     flash_lock(); }
由于flash的2kb页擦除特性,这里先将待修改代码区的flash页数据拷贝到缓冲buffer里,然后修改buffer里的数据,之后擦除flash相关页,最后将buffer里修改后的数据重新写回到flash里去。error_process函数的反汇编如下:
    $t     .arm.__at_0x8000800     error_process         0x08000800:    b570        p.      push     {r4-r6,lr}         0x08000802:    4d1a        .m      ldr      r5,[pc,#104] ; [0x800086c] = 0x8006000         0x08000804:    142a        *.      asrs     r2,r5,#16         0x08000806:    4629        )f      mov      r1,r5         0x08000808:    4819        .h      ldr      r0,[pc,#100] ; [0x8000870] = 0x20000008         0x0800080a:    f7fffcbd    ....    bl       __aeabi_memcpy ; 0x8000188         0x0800080e:    4818        .h      ldr      r0,[pc,#96] ; [0x8000870] = 0x20000008         0x08000810:    f8d00568    ..h.    ldr      r0,[r0,#0x568]         0x08000814:    f36f000f    o...    bfc      r0,#0,#16         0x08000818:    f2420164    b.d.    mov      r1,#0x2064         0x0800081c:    4408        .d      add      r0,r0,r1         0x0800081e:    4914        .i      ldr      r1,[pc,#80] ; [0x8000870] = 0x20000008         0x08000820:    f8c10568    ..h.    str      r0,[r1,#0x568]         0x08000824:    4608        .f      mov      r0,r1         0x08000826:    f8d00578    ..x.    ldr      r0,[r0,#0x578]         0x0800082a:    f36f000f    o...    bfc      r0,#0,#16         0x0800082e:    f2420164    b.d.    mov      r1,#0x2064         0x08000832:    4408        .d      add      r0,r0,r1         0x08000834:    490e        .i      ldr      r1,[pc,#56] ; [0x8000870] = 0x20000008         0x08000836:    f8c10578    ..x.    str      r0,[r1,#0x578]         0x0800083a:    f7fffd53    ..s.    bl       flash_unlock ; 0x80002e4         0x0800083e:    2035        5       movs     r0,#0x35         0x08000840:    f7fffcca    ....    bl       flash_clearflag ; 0x80001d8         0x08000844:    4628        (f      mov      r0,r5         0x08000846:    f7fffccd    ....    bl       flash_erasepage ; 0x80001e4         0x0800084a:    14ae        ..      asrs     r6,r5,#18         0x0800084c:    2400        .$      movs     r4,#0         0x0800084e:    e007        ..      b        0x8000860 ; error_process + 96         0x08000850:    4a07        .j      ldr      r2,[pc,#28] ; [0x8000870] = 0x20000008         0x08000852:    f8521024    r.$.    ldr      r1,[r2,r4,lsl #2]         0x08000856:    eb050084    ....    add      r0,r5,r4,lsl #2         0x0800085a:    f7fffd0d    ....    bl       flash_programword ; 0x8000278         0x0800085e:    1c64        d.      adds     r4,r4,#1         0x08000860:    42b4        .b      cmp      r4,r6         0x08000862:    d3f5        ..      bcc      0x8000850 ; error_process + 80         0x08000864:    f7fffcfe    ....    bl       flash_lock ; 0x8000264         0x08000868:    bd70        p.      pop      {r4-r6,pc}     $d         0x0800086a:    0000        ..      dcw    0         0x0800086c:    08006000    .`..    dcd    134242304         0x08000870:    20000008    ...     dcd    536870920
那么这124个字节就是最终要传输到0x8000800处的函数数据。传输完毕后软复位mcu,bootloader将app的flash数据进行篡改,达到改变程序功能的目的。
为什么要在bootloader运行时篡改app的数据?按理说在app运行时接收到error_process函数的更新数据后可以立刻运行,但是由于涉及到对app自身代码的修改,涉及flash修改的一些相关函数有可能会被暂时破坏而导致代码运行崩溃。  
四、跳过app的某些函数     如果想跳过“led_blings_1”函数,有2种方法:
1、函数内部跳过 即将以下汇编语句     0x0800655a:    2400        .$      movs     r4,#0     修改为     0x0800655a:    e013        .$      b             0x08006584 在“led_blings_1”函数入口处指令修改直接跳转到函数出口处。至于汇编的机器码和用法文末有相关资料可以查阅。
因为修改处的字节偏移为0x55a,是pagebuf下标为342元素的高2byte,需要在error_process函数中做如下修改:
pagebuf[342] = (pagebuf[342] & 0x0000ffff) | 0xe0130000;        
  2、函数调用处跳过 main函数汇编如下:
    $t     i.main     main         0x080065f8:    f44f41c0    o..a    mov      r1,#0x6000         0x080065fc:    f04f6000    o..`    mov      r0,#0x8000000         0x08006600:    f7fffe5c    ...    bl       nvic_setvectortable ; 0x80062bc         0x08006604:    2048        h       movs     r0,#0x48         0x08006606:    f7ffff01    ....    bl       systick_init ; 0x800640c         0x0800660a:    f7ffff85    ....    bl       init_led ; 0x8006518         0x0800660e:    f7ffffa3    ....    bl       led_blings_1 ; 0x8006558         0x08006612:    f7ffffbb    ....    bl       led_blings_2 ; 0x800658c         0x08006616:    f7ffffd3    ....    bl       led_blings_3 ; 0x80065c0         0x0800661a:    e011        ..      b        0x8006640 ; main + 72         0x0800661c:    f44f6180    o..a    mov      r1,#0x400         0x08006620:    4808        .h      ldr      r0,[pc,#32] ; [0x8006644] = 0x40010c00         0x08006622:    f7fffe43    ..c.    bl       gpio_setbits ; 0x80062ac         0x08006626:    f44f707a    o.zp    mov      r0,#0x3e8         0x0800662a:    f7ffff4f    ..o.    bl       delay_ms ; 0x80064cc         0x0800662e:    f44f6180    o..a    mov      r1,#0x400         0x08006632:    4804        .h      ldr      r0,[pc,#16] ; [0x8006644] = 0x40010c00         0x08006634:    f7fffe38    ..8.    bl       gpio_resetbits ; 0x80062a8         0x08006638:    f44f707a    o.zp    mov      r0,#0x3e8         0x0800663c:    f7ffff46    ..f.    bl       delay_ms ; 0x80064cc         0x08006640:    e7ec        ..      b        0x800661c ; main + 36     $d         0x08006642:    0000        ..      dcw    0         0x08006644:    40010c00    ...@    dcd    1073810432
下面是调用语句
    0x0800660e:    f7ffffa3    ....    bl       led_blings_1 ; 0x8006558
直接将此语句改为空语句nop(0xbf00)即可跳过调用,由于该命令占用4个字节,nop是两个字节的命令,所以替换为两个nop命令。
    0x0800660e:    bf00bf00    ....    nop        
因为修改处的字节偏移为0x60e,是pagebuf下标为387元素的高2byte和下标为388元素的低2byte,需要在error_process函数中做如下修改:
pagebuf[387] = (pagebuf[387] & 0x0000ffff) | 0xbf000000;  pagebuf[388] = (pagebuf[388] & 0xffff0000) | 0x0000bf00;


钽电容的命名详细介绍
机器视觉检测:表面检测常见缺陷有哪些
磁悬浮输送系统优势(四)绝对位置免回零,结构简单,节省空间
你会选择哪一家来办理ETC
5G助力泰国抗疫的三大用例
通过篡改特定代码数据修复单片机BUG的方法
兆芯新一代通用处理器正式推出超越E500-ZX新品国产整机
空气开关有几种保护_家用空气开关怎么选择
iphone8什么时候上市?iphone8最新消息:iphone8:前后双摄+Home保留,指纹后置画风突变
有方科技智慧出行解决方案助力智能网联汽车行业加速驶入5G时代
电动汽车常用的驱动系统介绍
三星 Galaxy S9/S9+ 评测:无愧机皇之名
小米6售价公布: 三个版本/2499起 将性价比进行到底
为什么说用过锤子,就会放弃苹果?坚果Pro告诉你锤子手机体验有多好!
什么是BPSK?
EPLAN2023数据与博途V18互通?
漏电检测仪如何使用_漏电检测仪的使用方法
四款红米新机曝光 K30 Pro变焦版摄像头或有惊喜
欧盟委员会发布《2018年欧盟工业研发投资排行》中国共有11家企业进入全球前100名
来自单模光纤和多模光纤自述