利用GNU poke修改二进制程序代码以避开麻烦的编译过程

正如GNU poke官网描述的那样,GNU poke是一个可扩展的二进制数据编辑器。笔者 也是刚好有机会实际用上它,因此记录一下使用它的前因后果。

近期在研究TlsrTools825x的代码,简单介绍一下这个项目:几款小米温度计使用 了Telink公司的TLSR825x系列芯片,而TLSR825x系列芯片拥有一种用于调试的 1-wire接口——SWS(Single Wire Slave),因此只要有能跟SWS接口进行沟通的硬件 就能对程序进行烧录或调试,但因为是Telink公司自己开发的沟通协议,所以支持 这个协议的硬件并不多。一般的解决方法是购买能支持的硬件,比如项目TLSRPGM 是基于TLSR芯片的编程器,而TlsrTools825x则是pvvx开发的基于STM32F103C8T6的 TLSR825x编程器。

使用TlsrTools825x的流程正如仓库中README所描述的那样,先将STM32F103C8T6开 发板和小米LYWSD03MMC温度计按照图中的方式连接,然后在Windows系统上运行仓 库中名为 TlsrTool.exe 的Windows GUI软件。在这一步确实笔者遇到了些困 难,因为笔者长期使用GNU/Linux系统,手边运行着Windows的设备少之又少,所以 不是十分情愿花时间准备一个Windows设备,哪怕是虚拟机。好在发现了有开发者 遇到了类似的情况,并且用Python写了一个cli客户端出来。rbaron的cli客户端是 为小米6手环内的TLSR8232准备的,不过我相信只需要修改一下就能给TLSR8251使 用。但是出现了一个问题,那就是TlsrTools825x多出来了一个配置GPIO PB0的步 骤,那就代表我需要在cli客户端中增加一段代码用来告诉我的STM32编程器要将 PB0配置成push-pull模式。通过rbaron的 tlsr82-debugger-client.pySTM32代码判断,编程器接上电脑后会显示为一个串口设备,每次对设备写入一串 字节串后,编程器会判断这个字节串是否是对编程器下的命令,如果是的话就会由 编程器执行对应的命令,如果不是的话,就会通过Telink的1-wire协议去输出这段 字节串。举个例子,如果字节串的第一个字节是 0x55 然后第二个字节是 0x00 的话,根据代码,编程器就会拉低GPIO PB0的电压,而如果第二个字节是 0x01 的话,就会拉高GPIO PB0的电压。就是说只要能找到让编程器设置PB0的指 令,就能用Windows GUI以外的方式设置PB0的模式了。但是笔者能力有限,不是很 能从Windows GUI应用设置PB0模式的代码中看出对应的指令:

// areg : 0..16 -> pa0..15, 16..31 -> pb0..15, 32..47 -> pc0..15          
procedure TfrmMain.SetGPIO(areg : dword);                                 
var                                                                       
dreg : dword;                                                             
cmd, idx : byte;                                                          
begin                                                                     
    if flgComOpen then begin                                              
      flgReadGpioOn := False;                                             
      PurgeCom(PURGE_TXCLEAR or PURGE_RXCLEAR);                           
      FormStmGpio.Caption := 'STM32F103C6T8 GPIO P'+Char(65 + (areg shr 4)) + IntToStr(areg and $0f);
      FormStmGpio.cnf_mode := (gpio.crx[areg shr 3]  shr ((areg and $07)*4)) and $0f;
      FormStmGpio.odr_bit := (gpio.odr[areg shr 4]  shr (areg and $0F)) and $01;
      if FormStmGpio.ShowModal = mrOk then begin                          
        // set/clr ord                                                    
        cmd := (areg shr 4) or $10;                                       
        idx := areg and $0f;                                              
        if FormStmGpio.odr_bit <> 0 then                                  
          dreg := 1 shl idx                                               
        else                                                              
          dreg := 1 shl (idx + 16);                                       
        if StmCmdPDWord(cmd, @dreg) then                                  
          gpio.odr[areg shr 4] := dreg;                                   
        // set mode/config crx                                            
        cmd := (areg shr 3) or $20; // cmd crx &= data                    
        idx := (areg and $07) * 4;                                        
        dreg := not ($0f shl idx);                                        
        if StmCmdPDWord(cmd, @dreg) then                                  
          gpio.crx[areg shr 3] := dreg;                                   
        if FormStmGpio.cnf_mode <> 0 then begin                           
          cmd := (areg shr 3) or $30; // cmd crx |= data                  
          dreg := (FormStmGpio.cnf_mode and $0f) shl idx;                 
          if StmCmdPDWord(cmd, @dreg) then                                
            gpio.crx[areg shr 3] := dreg;                                 
        end;                                                              
      end;                                                                
    end;                                                                  
    Timer.Enabled := CheckBoxChkGpio.Checked;                             
    if not Timer.Enabled then ShapeRun.Brush.Color := clSilver            
    else ShapeRun.Brush.Color := clYellow;                                
    flgReadGpioOn := True;                                                
end;                                                                      

procedure TfrmMain.ShapePB0MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);                                     
begin                                                                     
      if Button = mbRight then begin // mbLeft,  mbMiddle                 
        SetGPIO(16);                                                      
      end;                                                                
end;

那么还有一个选择,那就是修改编程器的代码,让编程器直接将PB0设置成 push-pull模式,这样子就能免去读代码的麻烦。翻了下代码,能看到初始化PB0的 代码在这里

/* конфигурация вывода PB0 на выход c OK на RST модуля */
GPIO_InitStruct.Pin = GPIO_PIN_0; // номер вывода
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // режим выхода
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM; // средняя скорость выхода
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);

GPIO_MODE_OUTPUT_OD 代表open-drain模式,而 GPIO_MODE_OUTPUT_PP 则 代表push-pull模式,代码

#define  GPIO_MODE_INPUT                        ((uint32_t)0x00000000)   /*!< Input Floating Mode                   */
#define  GPIO_MODE_OUTPUT_PP                    ((uint32_t)0x00000001)   /*!< Output Push Pull Mode                 */
#define  GPIO_MODE_OUTPUT_OD                    ((uint32_t)0x00000011)   /*!< Output Open Drain Mode                */
#define  GPIO_MODE_AF_PP                        ((uint32_t)0x00000002)   /*!< Alternate Function Push Pull Mode     */
#define  GPIO_MODE_AF_OD                        ((uint32_t)0x00000012)   /*!< Alternate Function Open Drain Mode    */
#define  GPIO_MODE_AF_INPUT                     GPIO_MODE_INPUT          /*!< Alternate Function Input Mode         */

那么就简单多了,只要修改源代码把初始化PB0时的 GPIO_MODE_OUTPUT_OD 换成 GPIO_MODE_OUTPUT_PP 就可以了,然后将修改好的代码编译再烧录到编程器里就 可以了。但是又出现了一个问题,就是对这份代码对于笔者来说并不算方便;在编 程器的代码仓库中能看到一个叫 MDK_ARM 的文件夹,简单搜索一下会发现这代 表这份代码是使用Keil开发工具进行开发的,那么想要编译的话最好是能下载对应 的工具。很遗憾,笔者并不乐意为了这点小事去往自己的电脑里下载那么多不熟悉 的东西。如果一个软件在Debian官方的打包仓库找不到的话,那么我会尽可能避免 安装它。

这时候就能有利用GNU poke这个二进制编辑器的机会了。TlsrTools825x代码仓库 中是有一份编译好的固件代码的。只要能够找到这一行代码对应的机器码,就能利 用二进制编辑器直接修改:

GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // режим выхода

事先声明 : 不经过任何代码检查或基本的安全验证就直接使用编译好的软件在计算机安全方 面并不是一个好的习惯,在这里直接这么干只是因为笔者认为这块STM32编程器如 果真的运行了出现问题的代码,那问题对我来说也是微不足道的。而如果直接修改 和运行有效果,那么就能省去一些看代码的时间。

TlsrTools825x仓库中的固件代码是一个hex文件,使用intelhex的工具能够将hex 文件转换成bin文件:

$ file USB2Swire-STM32F103C8-v06.hex 
USB2Swire-STM32F103C8-v06.hex: Intel hexadecimal object, 0x02 record length, 0x0000 offset, '04' type, data+checksum 0800F2
$ hex2bin.py USB2Swire-STM32F103C8-v06.hex USB2Swire-STM32F103C8-v06.bin
$ file USB2Swire-STM32F103C8-v06.bin 
USB2Swire-STM32F103C8-v06.bin: ARM Cortex-M firmware, initial SP at 0x200017c8, reset at 0x08000100, NMI at 0x0800171e, HardFault at 0x08001656, SVCall at 0x08001bfc, PendSV at 0x08001aa2

然后接下来的问题就是该怎么定位到目标代码了,首先先用binutilsarm-none-eabi-objdump 把bin文件反汇编一下:

arm-none-eabi-objdump -D -bbinary -m armv7 -M force-thumb USB2Swire-STM32F103C8-v06.bin

STM32F103C8T6使用Arm Cortex M3处理器,而这个处理器的CPU架构是Armv7-M,就 代表它只支持Thumb指令集。因此 objdump 使用了 -m armv7 定义架构, -M force-thumb 代表强制把数据都看作Thumb指令。

接下来就能开始定位了,我们能利用这行代码来定位:

GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // режим выхода                                      

GPIO_MODE_OUTPUT_OD 的值是 0x11 ,十进制就是 17 ,那么在反汇编后会 显示为 #17 。运气不错, #17 并不算是一个常见的值,那么在反汇编的内容 中搜索一下 #17 ,其中这段内容吸引了我的注意:

16c6:	f44f 5400 	mov.w	r4, #8192	@ 0x2000
16ca:	9004      	str	r0, [sp, #16]
16cc:	2200      	movs	r2, #0
16ce:	4621      	mov	r1, r4
16d0:	4628      	mov	r0, r5
16d2:	f7ff f86f 	bl	0x7b4
16d6:	4e10      	ldr	r6, [pc, #64]	@ (0x1718)
16d8:	2201      	movs	r2, #1
16da:	4611      	mov	r1, r2
16dc:	4630      	mov	r0, r6
16de:	f7ff f869 	bl	0x7b4
16e2:	9400      	str	r4, [sp, #0]
16e4:	2401      	movs	r4, #1
16e6:	9401      	str	r4, [sp, #4]
16e8:	4669      	mov	r1, sp
16ea:	4628      	mov	r0, r5
16ec:	9403      	str	r4, [sp, #12]
16ee:	f7fe ff7f 	bl	0x5f0
16f2:	2011      	movs	r0, #17
16f4:	e9cd 4000 	strd	r4, r0, [sp]
16f8:	4669      	mov	r1, sp
16fa:	4630      	mov	r0, r6
16fc:	9403      	str	r4, [sp, #12]
16fe:	f7fe ff77 	bl	0x5f0
1702:	2201      	movs	r2, #1
1704:	4611      	mov	r1, r2
1706:	4630      	mov	r0, r6
1708:	f7ff f854 	bl	0x7b4

其中的 movs r0, #17 和随后出现的 str 让人想到附值操作;一般对变量 附值的操作在汇编中就会体现为先把值移到一个寄存器中,然后用 store 类型 的指令把寄存器中的值存到内存中。除此之外,还有一个重要证据就是这个指令: mov.w r4, #8192 @ 0x2000 。在对PB0初始化之前,还有一段对 PC13初始化的代码,而 GPIO_PIN_13 的值就是 0x2000 。所以由此推断,只 要将bin文件中地址为 16f2 的数据 2011 改成 2001 ,就能将PB0设置为 push-pull模式,因为push-pull模式的值0x1

接下来定位好了,那么只要用GNU poke修改数据就行了。GNU poke功能很多,这里 只简单演示一下基本的编辑功能,具体信息可以用 info poke 翻阅。

$ poke USB2Swire-STM32F103C8-v06.bin
(poke) byte @ 0x16f2#B
17UB
(poke) dump :from 0x16f2#B
76543210  0011 2233 4455 6677 8899 aabb ccdd eeff  0123456789ABCDEF
000016f2: 1120 cde9 0040 6946 3046 0394 fef7 77ff  . ...@iF0F....w.
00001702: 0122 1146 3046 fff7 54f8 06b0 70bd 0010  .".F0F..T...p...
00001712: 0240 0010 0140 000c 0140 fee7 7047 0907  .@...@...@..pG..
00001722: 090e 0028 06da 00f0 0f00 00f1 e020 80f8  ...(......... ..
00001732: 141d 7047 00f1 e020 80f8 0014 7047 2de9  ..pG... ....pG-.
00001742: fc5f 4ff0 0008 0446 adf8 0480 adf8 0080  ._O....F........
00001752: 48f6 0f7b 4ff0 0209 4ff4 8066 92e1 bdf8  H..{O...O..f....
00001762: 0410 11f0 0f07 2bd0 30f8 2710 adf8 0010  ......+.0.'.....
(poke) byte @ 0x16f2#B
17UB
(poke) byte @ 0x16f2#B = 0x1
(poke) .quit 

这样就编辑好了,再次使用 objdump 就能看到数值被修改了:

16ea:	4628      	mov	r0, r5
16ec:	9403      	str	r4, [sp, #12]
16ee:	f7fe ff7f 	bl	0x5f0
16f2:	2001      	movs	r0, #1
16f4:	e9cd 4000 	strd	r4, r0, [sp]
16f8:	4669      	mov	r1, sp
16fa:	4630      	mov	r0, r6

接下来只要把修改好的代码烧录到STM32编程器中,然后在启动后配合万用表检查 一下PB0是否有被配置为push-pull模式就可以了。