单片机main函数退出后发生什么——以stm32为例
STM32:main函数退出后发生什么?
我们都在说单片机要运行在无限循环里,不能退出,可退出之后会发生什么?
讨论STM32启动过程的文章数不胜数,可main函数结束之后会发生什么却少有讨论。
几日前突然想到这个问题,便开始了探究。
如果不想看冗长的调查和实验过程,可以直接到文章底部看结论,也有流程图版哦。
网上搜索
可能因为大家不太关心这种情况,我没有找到有关论述单片机main函数退出的文章。不过在ST Community、阿莫BBS、StackOverflow看到有人在问同样的问题,下面摘录了一些不同角度的回答:
- C语言环境角度,三种可能性
- 编译器在main函数后加入隐性的无限循环
- 编译器在main外面添加一层无限循环
- CPU继续向下取址运行(也就是跑飞了)
- 单片机设计角度,退出会引发异常、事件等
- 实际测试,网友们得到的结果却不太一样
- 有的会自动循环,像是自动复位了
- 有的会循环同一段汇编
可以看出,答案众说纷纭,并没有权威性,于是就转向了最权威的资料:Keil手册,arm官方工具链文档。
文档查阅
为了寻找main外面的调用情况,我们要从熟悉的启动代码开始:
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
我们知道,是__main
调用了用户main函数,在手册1.8.1 Initialization of the execution environment and execution of the application
这一小节,概述了__main
的作用:
- 复制RO和RW段的内容,必要的话进行解压缩
- 初始化ZI段(置零)
- 调用
__rt_entry
那这个__rt_entry
是十分的重要啊!
按图索骥,__rt_entry
的功能有以下几点:
- 调用函数初始化堆栈
- 初始化C库,runtime
- 调用用户的main
- Calls exit() with the value returned by main()
情况不唯一,第四步的exit()可以换为另外两个退出函数,他们三个退出函数的关系在后面会提到。
情况变得明朗起来,只要找到这个exit()
调用的实现即可。我在stdlib.h中找到了exit
的声明:
extern _ARMABI_NORETURN void exit(int /*status*/);
/*注释有删减,删掉了不少次要内容,有兴趣可以去看一看。
* First, all functions registered by the atexit function are called.
* Next, all open output streams are flushed, all open streams are closed,
* and all files created by the tmpfile function are removed.
* Finally, control is returned to the host environment.
*/
总结下来有三个功能:1. 调用之前注册过的atexit函数 2. 关闭C运行时 3. 向宿主环境上交控制权。
然而具体实现细节还是未知的,我们回到__rt_entry
的文档中看看:
最后一步,必须调用exit
、__rt_exit
、_sys_exit
三个中的一个。然而仔细观察他们三个的功能,是不是能察觉出一丝重复的意味。在功能上,exit
包含__rt_exit
包含_sys_exit
,显然他们三个不会是毫无关联的。
在阅读完所有相关文档后,我们能得出结论:exit
调用__rt_exit
调用_sys_exit
,后面实验中的汇编也印证了这一点。
然而,其中的_sys_exit
是不是看起来很眼熟呢?相信用过STM32的朋友都了解串口打印调试与printf
函数重定向(只讨论不使用microlib的情况),其中会有这样一段函数定义:
void _sys_exit(int x) //避免半主机模式
{
x = x;
}
如果阅读了1.6.4 Using the libraries in a nonsemihosting environment
这一节,我们就会发现_sys_exit
是典型的依赖半主机模式的调用。因为启动代码中的函数一路调用会调用到_sys_exit
上去,所以在非半主机模式下我们需要自己提供它的定义。
Semihosting,半主机模式会把标准C库中的一些应该提供的函数使用特定的指令交给调试主机来实现。由
1.8.5 Direct semihosting C library function dependencies
可知这些函数包括:
_sys_exit
_sys_close
_sys_open
_sys_write
等,在半主机模式下,对这些函数直接或者间接的调用将转化为特定的指令。在非半主机模式下,就需要手动实现被调用的函数。
半主机作为一种调试手段,听起来非常诱人,ARM自己的Keil MDK竟然不支持。既然半主机模式影响了必然会被调用的_sys_exit
,那就会影响到main函数退出之后的动向。在下一节的实测中,也确实体现出了巨大的差异。
实验测试
芯片:STM32F407ZGT6
仿真器:DAP-Link
环境:ARMCC V5.06 update 6 ,Keil 5.25.2.0 , -O0
main函数内容如下:
int main(void){
GPIO_InitTypeDef GPIO_Initure;
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(336,8,2,7); //设置时钟,168Mhz
__HAL_RCC_GPIOF_CLK_ENABLE(); //开启GPIOF时钟
GPIO_Initure.Pin=GPIO_PIN_9|GPIO_PIN_10; //PF9,10
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH;
//开灯
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_RESET);
HAL_Delay(1000);
//关灯
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_9,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_10,GPIO_PIN_SET);
HAL_Delay(1000);
}
PF9和PF10是开发板上两颗LED灯,能提供直观展示。
非半主机
现象:两颗LED不断闪动,就像处于循环之中。
为了找出原因,自然是要开始打断点+单步调试汇编。
此时的汇编是这样的:
继续向下取址的话,接下来会弹栈,也将返回到调用main
的函数__rt_entry
中:
这也印证了之前的推断,在默认情况下,调用的是exit
。实际运行与之前分析一致,exit
调用__rt_exit
调用_sys_exit
。
最后调用的,我们自己定义的_sys_exit
,可以看出x=x
被编译器优化成为一句空指令。
重点在于接下来,按照手册上说,_sys_exit
将会把控制权交回宿主环境,此时C运行库已经被关闭。然而下一句汇编BX lr
直接将函数返回0x08000227
,也就是__rt_exit
函数调用_sys_exit
的下文。在上上张图中,可以发现代码又回到了熟悉的启动代码,接下来,时钟、堆栈、C库依次初始化,main函数被调用,形成循环。
这就是退出主函数后表现为循环的原因。
半主机
如果想进入半主机模式,我们可以将#pragma import(__use_no_semihosting)
这句宏删除,之后把自定义的_sys_exit
等函数注释掉,再进行编译、下载、调试。
现象:LED灯亮灭一次,无后序现象。
启动以及退出流程与非半主机完全一样,除了在调用_sys_exit
时会变为相应的内核特定指令。ARM处理器在进入半主机模式时会调用trap instruction
,对于所有的Cortex-M
微处理器来说,这个指令是BKPT 0xAB
。紧接着,就进入了跳转到自己的死循环。
至此,单片机陷入空白死循环,形成了前文所说的执行一次现象。
结论
通过查阅官方文档,以及调试实测,我们能得出结论:
在关闭半主机模式下,STM32的用户main函数退出了,单片机将会复位,形成循环的效果。开启半主机模式下,如果退出主程序,会在空循环卡死,表现为只会执行一遍主函数内容。补充一点,如果使用微库(microlib),文档中明文禁止退出main函数。
使用流程图表示如下:
这篇文章所讨论的退出主函数,对于没有OS的单片机来说,可以说是一种未定义行为,本身是不安全的、不被推荐的。以上的讨论与实验。虽然实用性不高,但在学习过程中仍有不少的收获。
这个主题原本是偶然想到的,花费了一些精力,算把这个问题弄清楚一些了。同时,在这个过程中产生了更多的疑问:将启动代码放置在_sys_exit
之后是ARM还是ST的安排?是在哪一步实施的?文中的实验具有普适性吗?等等疑问还等待着解答。
技术新人,水平有限,希望各位前辈、高人不吝赐教,如有错误请一定指出。更多嵌入式原创文章可以来公众号,来找我聊聊天吧:
欢迎转载,请注明作者与原文链接。
作者:胡小安
原文链接:https://www.cnblogs.com/huxiaoan/p/15821662.html
单片机main函数退出后发生什么——以stm32为例的更多相关文章
- 设计main函数退出后继续执行一段代码
原理: 使用 _onexit() 函数注册一个函数,这个函数会在main函数退出后执行 使用原则: 1.包含在cstdlib中,是c语言中的库函数: 2.需要注册的函数格式为:int类型返回值.无参数 ...
- 【记录一个问题】cuda核函数可能存在栈溢出,导致main()函数退出后程序卡死30秒CUDA
调试一个CUDA核函数过程中发现一个奇怪的问题:调用某个核函数,程序耗时33秒,并且主要时间是main()函数结束后的33秒:而注释掉此核函数,程序执行不到1秒. 由此可见,可能是某种栈溢出,导致了程 ...
- [牛客网试题] Test.main() 函数执行后的输出是()
public class Test { public static void main(String [] args){ System.out.println(new B().getValue()); ...
- linux编程之main()函数启动过程【转】
转自:http://blog.csdn.net/gary_ygl/article/details/8506007 1 最简单的程序 1)编辑helloworld程序,$vim helloworld. ...
- 转:iOS程序main函数之前发生了什么
原文地址:http://blog.sunnyxx.com/2014/08/30/objc-pre-main/ 我是前言 一个iOS app的main()函数位于main.m中,这是我们熟知的程序入口. ...
- iOS程序main函数之前发生了什么
我是前言 一个iOS app的main()函数位于main.m中,这是我们熟知的程序入口.但对objc了解更多之后发现,程序在进入我们的main函数前已经执行了很多代码,比如熟知的+ load方法等. ...
- main函数执行前后还会发生什么
问题分析 首先main()函数只不过是提供了一个函数入口,在main()函数中的显示代码执行之前,会由编译器生成_main函数,其中会进行所有全局对象的构造以及初始化工作.简单来说对静态变量.全局变量 ...
- C++程序设计基础(8)main函数
注:读<程序员面试笔记>笔记总结 1.知识点 (2)main函数的形式 //first type int main() //second type int main(int argc,ch ...
- c语言main函数返回值、参数详解(返回值是必须的,0表示正常退出)
C语言Main函数返回值 main函数的返回值,用于说明程序的退出状态.如果返回0,则代表程序正常退出:返回其它数字的含义则由系统决定.通常,返回非零代表程序异常退出. 很多人甚至市面上的一些书籍,都 ...
随机推荐
- CF938A Word Correction 题解
Content 有一个长度为 \(n\) 的,只包含小写字母的字符串,只要有两个元音字母相邻,就得删除后一个元音字母(\(\texttt{a,e,i,o,u,y}\) 中的一个),请求出最后得到的字符 ...
- CF1110A Parity 题解
Content 求下面式子的奇偶性,其中 \(a_i,k,b\) 会在输入中给定. \[\sum\limits_{i=1}^k a_i\cdot b^{k-i} \] 数据范围:\(2\leqslan ...
- 针对HttpClient 重写 HttpRequestRetryHandler针对特定异常 增加重试
调用方法: public static String doGet(String url) { try { RequestConfig defaultRequestConfig = RequestCon ...
- ssh框架从页面传中文发生乱码时怎么解决,就是添加一个字符编码拦截器。用springframework自带的便可
ssh框架从页面传中文发生乱码时怎么解决,就是添加一个字符编码拦截器.用springframework自带的便可
- 事件处理 及冒泡 阻止默认事件 以及tab 切换的思路
1.axios post通过点击事件提交数据不需要使用input直接使用state2.pdd你好天天象上默认执行点击(1,2,3)也可以执行并且能切换页码3.tab 针对新闻不同时4.天天象上首页和精 ...
- SpringBoot单元测试demo
引入maven <dependency> <groupId>org.springframework.boot</groupId> <artifactId> ...
- SpringBoot统一日志打印
统一日志打印 @Slf4j @Aspect @Component public class ControllerLog { private static final ThreadLocal<Lo ...
- World is Exploding(hdu5792)
World is Exploding Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Other ...
- Adversarial Defense by Restricting the Hidden Space of Deep Neural Networks
目录 概 主要内容 Mustafa A., Khan S., Hayat M., Goecke R., Shen J., Shao L., Adversarial Defense by Restric ...
- IT6516DP转VGA转换器|替代台湾联阳IT6516方案|CS5212Capstone
台湾联阳IT6516是一种高性能的DP显示端口到VGA转换器方案芯片.IT6516结合DisplayPort接收器和三重DAC,通过转换功能支持DisplayPort输入和VGA输出.内置Displa ...