一种通过篡改特定代码数据修复嵌入式产品BUG的方法

2023-11-01

一、前言

        在嵌入式产品开发中,难以避免地会因为各种原因导致最后出货的产品存在各种各样的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 Obj\Template.axf

fromelf --bin --output=test.bin Obj\Template.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 < 8; i++)
		{
			NVIC->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.
	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; 

        记得之前看过软件破解的一些教程,教怎么去绕过一些验证的,比较简单的方法也是这种类似的操作,通过修改验证入口来跳过验证。

五、总结

        mcu跑程序无非就是按照固有的机制去取指令然后运行,我们升级代码往往是整套代码完全替换,本文提出一种通过对原代码少量Flash数据进行直接篡改的方式来修复固件bug的方法,这也许可以作为一个产品的后门使用。本文为了简单只演示核心想法,具体流程的完备性未做考虑。

六、参考资料

《ARM指令计算机器码,自己归纳整理的ARM THUMB指令机器码表》

《thumb长跳转指令(BL)机器码详解》

《armv7-A系列5- arm 指令集以及编码》

《armlink使用方法详解》

bootloader.rar

app.rar

链接:https://pan.baidu.com/s/10puoQW5YI7TKDH8jlr-Gmg  提取码:q6er

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

一种通过篡改特定代码数据修复嵌入式产品BUG的方法 的相关文章

随机推荐

  • CNN图片分类(Pytorch)

    这篇文章主要讲述用 pytorch 完成简单 CNN 图片分类任务 如果想对 CNN 的理论知识进行了解 可以看我的这篇文章 深度学习 一 CNN卷积神经网络 图片分类 我们以美食图片分类为例 有testing training valid
  • Ubuntu搭建开发openchannelssd的qemu的虚拟机

    1 安装带有NVMe支持的qemu QEMU Installation QEMU support for Open Channel SSDs is based on top of Keith Busch s qemu nvme branch
  • Selenium

    第一章 Selenium 概述 1 1 Selenium 发展史 Selenium是一系列基于Web的自动化工具 提供一套测试函数 用于支持Web自动化测试 函数非常灵活 能够完成界面元素定位 窗口跳转 结果比较 具有如下特点 多浏览器支持
  • 区块链笔记分享:

    区块链笔记分享 技术和数学基础 1 高位的hash的逆向过程除了穷举 没有更有效的办法 这个过程在目前的计算能力下必然费时费力 2 不对称的加密 公钥和私钥的签名机制 PoW 1 记帐中调整once 得到符合规则的所花费的工作 2 谁计算得
  • brew install下载资源网络总是中断怎么办

    问题 在mac系统下用brew install命令安装软件的时候 下载资源总是下到一半就中断了 导致一直安装不上 解决思路 先手动下载安装资源 再执行brew install安装 具体流程 1 下载对应资源 brew下载资源失败的时候会提示
  • 【Python开发】一文详解Flask-Login

    一文详解Flask Login Flask Login 为 Flask 提供用户会话管理 它处理登录 注销和长时间记住用户会话等常见任务 Flask Login 不绑定到任何特定的数据库系统或权限模型 唯一的要求是您的 用户对象实现一些方法
  • JUC并发编程(超详细)

    随着对Java的深入学习 越发觉得JVM和JUC这些底层原理的重要性 在看完黑马程序员的JVM后 也对JUC产生了浓厚的兴趣 在学习JUC的过程中 通过老师的视频做笔记 在遇到一些不懂的或者想更深入了解的知识时 自己也去搜索了很多资料 再把
  • MySQL数据库-更新表中的数据详解

    更新书记记录是数据操作中常见的操作 可以更新表中已经存在数据记录中的值 在MySQL中可以通过UPDATE语句来实现更新数据记录 该SQL语句可以通过如下几种方式使用 更新特定数据记录 更新所有数据记录 更新特定数据记录 在MySQL中更新
  • java数组初始化

    package equality public class demo5 public static void main String args int a1 1 2 3 4 5 int a2 a2 a1 将a1的数组赋给a2两者指向同一个数
  • 脚本检查 CentOS 系统信息

    脚本信息 bin bash auth lsr zds func sys info check version v1 0 sys centos6 x 7 x id u gt 0 echo 请用root用户执行此脚本 exit 1 sysver
  • 解决scrapy不执行Request回调函数callback

    情况1 未完成函数 测试函数 那个函数没有任何结果返回 可能这就是一个测试函数或是没写完的函数 因此只需要在这个函数结尾加一个yield就可以了 回调另一个空的函数就不会有任何影响 coding gbk def parse self res
  • javascript中获取上传文件的文件名称

    有时js中需要获取刚上传的文件的文件名称 都是带有后缀的 现在需要获取的是不带后缀名的 var file name1 上传的文件名称 带后缀名 var file name2 需获取的文件名 不带后缀名 var suffix 后缀名 file
  • windows10+libtorch1.0.0 cpu版本+opencv4.0.0 cmake安装编译

    目录 前期准备 配置CmakeLists cmake编译所需目录结构 cmake编译 参考 前期准备 下载cmake https cmake org download 安装时手动更改选项以添加到环境变量中 下载libtorch https
  • idea简便导入jar包的方法

    idea简便导入jar包的方法 Step1 2021 12 18 15 21 19 复制准备好的jar包 Step 2 2021 12 18 15 21 20 在需要该jar包的项目中创建一个文件夹 一般习惯文件夹的名字为lib Step
  • case when 多个条件 以及case when 权重排序

    1 case when 多个条件 语法 SELECT nickname user name CASE WHEN user rank 5 THEN 经销商 WHEN user rank 6 THEN 代理商 WHEN user rank 7
  • 【计算机视觉这一年】万字长文盘点近百篇代表论文、应用和市场

    新智元导读 The M Tank发布了一份对计算机视觉领域最近一年进展的报告 A Year in Computer Vision 详述了四大部分的内容 包括 分类 定位 目标检测 目标追踪 分割 超分辨率 自动上色 风格迁移 动作识别 3D
  • 头哥作业:统计字母数量

    统计字母数量 输入格式 输出格式 示例 1 任务描述 读取附件是一篇英文短文 请编写程序统计这篇短文前 n 行中每一个英文字母出现的次数 结果按次数降序排列 次数相同时 按字母表顺序输出 若 n 值大于短文行数 输出整篇文章中每一个英文字母
  • windows安装JDK、maven 和 IDEA

    一 JDK安装 Java程序必须运行在JVM之上 或者说java exe就是JVM 所以 使用Java的第一件事情是安装JDK 1 获取安装包 官方网址 https www oracle com java technologies down
  • 距离公式详解

    在做分类时常常需要估算不同样本之间的相似性度量 SimilarityMeasurement 这时通常采用的方法就是计算样本间的 距离 Distance 采用什么样的方法计算距离是很讲究 甚至关系到分类的正确与否 本文的目的就是对常用的相似性
  • 一种通过篡改特定代码数据修复嵌入式产品BUG的方法

    一 前言 在嵌入式产品开发中 难以避免地会因为各种原因导致最后出货的产品存在各种各样的BUG 通常会给产品进行固件升级来解决问题 记得之前在公司维护一款BLE产品的时候 由于前期平台预研不足 OTA参数设置不当 导致少数产品出现不能OTA的