写在前面
前两天在一个讨论群里面,一位同行提了一个问题,就是能不能程序跑起来后擦除某个指定的函数。诶,这问题可以啊,虽然我没有使用这个功能的需求,但是有人问那肯定就有人用,正好公司也没啥事,就花了两个小时鼓捣了一下,成功的实现了,下面记录一下操作的方法。
我的想法
看到这个问题,我的第一反应就是通过分散加载脚本来实现(具体是什么,自行网上搜索),因为之前搞过uboot
(uboot
里面一般称作链接脚本),根据之前的经验,觉得smt32
应该也是可行的,只需要知道函数存储的起始地址和结束地址就行了(但实际上操作起来是不需要就可以完成的,实在想要起始地址和结束地址,那得使用汇编了),而链接脚本可以得到这些信息。
第一步
准备一个简单的能运行的工程,搭建工程这些就不讲了,我直接打开了一个示例工程,直接奔主题,到底怎么做。
下图步骤2去掉勾选后才能使用指定的分散加载文件,否则会使用默认的(工程需要先编译一次才会生成这个脚本)。
第二步
打开分散加载文件后,新增一段对于ROM
的描述,我这里主控芯片是stm32f429
,选择第三个扇区来存储我需要擦除的函数,为什么这样选择呢?因为stm32 ROM
区的擦除是以扇区为最小单位,直接用整个扇区来存储我想擦除的内容,这样就比较简单,如果不设置为一个扇区,那可能擦除是比较麻烦的,需要先读回然后修改再写入。
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08000000 0x00200000 { ; load region size_region
ER_IROM1 0x08000000 0x00200000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x00030000 { ; RW data
.ANY (+RW +ZI)
}
}
;新增描述
LR_IROM2 0x0800C000 0x00004000 { ; load region size_region
ER_IROM2 0x0800C000 0x00004000 { ; load address = execution address
test_func.o (+RO)
}
}
如上,我新增了一段描述,将test_func.c
(对应test_func.o
)文件内的所有函数及只读内容全部放到指定的0x0800C000
地址开始的一片ROM
空间。
现在重新编译程序,打开生成的.map
文件就可以看到test_func.c
里面的函数地址都落在了指定的那一片区域,你也可以直接运行程序,把函数地址给打印出来查看。
另外一种方式
上面的方式已经能够实现将函数放到指定的地址了,但还是不够人性化,因为必须得将所有需要擦除的函数都放到一个指定的.c
文件内,这样就破坏了程序原有的结构和可读性,下面介绍另一种方式,那就是通过attribute
属性来指定函数存放的段,同一个段属性的内容会被放到一起。
;新增描述
LR_IROM2 0x0800C000 0x00004000 { ; load region size_region
ER_IROM2 0x0800C000 0x00004000 { ; load address = execution address
*(.TEST)
}
}
如上方式就是新增了一个TEST
段,具有这个段属性的内容就会被放到一起,而且是在指定的那一片ROM
区域内,示例用法:
__attribute__((section (".TEST")))
void test_func(void)
{
printf("%s\r\n", __FUNCTION__);
}
这种方式也是可行的,而且不需要指定文件,可以是任意.c
文件内的任意函数,和前面介绍的指定文件的方式是可以混用的。
另另一种方式
嗯,还有一种方式,这种方式不需要修改分散加载脚本就可以实现,不过也有其局限性,方法就是直接通过attribute
属性指定函数存放的地址,示例代码:
__attribute__((section (".ARM.__at_0x0800C000")))
void test_func(void)
{
printf("%s\r\n", __FUNCTION__);
}
和第二种方式是不是很像,但这种方式不好的地方就是,只指定了起始地址,你要是有很多个函数那就不好搞了,你还要自己去算各个函数的起始地址该设置为多少才不会冲突,如果冲突,编译时会报错。
结尾
好了,上面介绍了几种将函数放到指定位置的方法,那程序跑起来后,删除指定的函数就很简单了,就调下操作flash
的接口就行了,此处不作介绍。
思考
存放函数块的那一片区域被擦除了,那函数还能运行吗?
肯定不能了,所以需要在每个调用的地方判断一下,示例代码:
if(*(volatile uint32_t *)test_func != 0xffffffff)
{
test_func();
}
如果程序不是放在IROM
然后本地执行的,那上面的方法还有效吗?
不一定了,对于不支持XIP
的存储器来说,想要实现这个操作,那就不是这几步这么简单就能实现的了。
顺便提一下,我把之前很多分享的源码整理成了独立的项目,并且会一直维护,有需要的可以点击下方阅读原文访问文中超链接。(点我跳转)
欢迎扫码关注我的微信公众号
