【说在前面的话】


很多人对编译器优化等级0("-O0")有着谜之信仰——认为在这个优化等级下编译器一定不会对代码进行不必要的优化——至少不会进行危险且激进的优化。让我们来看一个来自Arm Compiler 5的案例吧:

【正文】


在嵌入式系统中通过属性weak(实际使用的时候很可能用gcc的兼容写法通过 __attribute__((weak)) 来给函数附加这一属性)来为某一个函数提供一个默认实现,实际上大家熟悉的中断处理程序就是这么实现的,比如随便打开一个startup_xxxx.S文件,我们可以看到如下的内容:

; Vector Table Mapped to Address 0 at Reset                AREA    RESET, DATA, READONLY                EXPORT  __Vectors                EXPORT  __Vectors_End                EXPORT  __Vectors_Size__Vectors       DCD     __initial_sp               ;     Top of Stack                DCD     Reset_Handler              ;     Reset Handler                DCD     NMI_Handler                ;     NMI Handler                DCD     HardFault_Handler          ;     Hard Fault Handler                ...                DCD     SysTick_Handler            ;     SysTick Handle...; Dummy Exception Handlers (infinite loops which can be modified)NMI_Handler     PROC                EXPORT  NMI_Handler               [WEAK]                B       .                ENDPHardFault_Handler\                PROC                EXPORT  HardFault_Handler         [WEAK]                B       .                ENDPSysTick_Handler PROC                EXPORT  SysTick_Handler           [WEAK]                B       .                ENDP

上述代码使用汇编语言的形式描述了一个典型的中断向量表:

  • 跟在 DCD 后面的是中断/异常处理函数的名称,比如,SysTick_Handler。将中断处理程序的名称放在 DCD 后面实际上相当于1)C语言中对目标函数取地址;2)然后将获取的函数的地址值作为uint32_t类型的常量替换掉“DCD”——也就是作为地址常数保存在中断向量表里;

  • 上述代码提供了 SysTick_Handler 等异常/中断处理函数的默认实现,这里特别用 “[WEAK]” 加以修饰,表示:如果用户实现了一个同名的函数,则在链接阶段(linking stage)使用用户提供的版本,并舍弃这个默认实现;相反,如果用户并没有提供一个同名的函数,则继续由这个默认实现的异常/中断处理函数来填补空缺。

正是借助了这样的便利,大家可以大大方方的在C语言中“按需”添加自己的中断处理程序,例如,下面的代码就通过SysTick_Handler实现了一个简单阻塞式的毫秒级延时功能:

#include ...static volatile uint32_t s_wMSCounter = 0;void SysTick_Handler(void){    if (s_wMSCounter) {        s_wMSCounter--;    }}void delay_ms(uint32_t wMillisecond){    s_wMSCounter = wMillisecond;    while( s_wMSCounter > 0 );}//! 用 constructor 修饰,会告诉编译器进入main函数之前一定先执行下对应的函数__attribute__((constructor(255)))void platform_init(void){    ...    /* Generate interrupt each 1 ms  */    SysTick_Config(SystemCoreClock / 1000);        ...}

毋庸置疑,最终上述代码中所实现的 SysTick_Handler() 会替换掉 startup_xxxxx.S 中所提供的那个默认的版本。到目前为止一切看起来也都还没什么问题。

更进一步的,假设我们想把上述代码封装成一个模块(无论该模块是提供源代码还是只提供库文件"*.lib")——也就是放在一个专门的“.c”文件中,然后就不希望模块的使用者去修改它的内容。这时候可能就会产生一个新的需求,因为这个模块用SysTick产生了一个1ms为间隔的中断,而系统中其它部分可能也需要这样一个1ms为间隔的事件源:一方面考虑只为了一个delay_ms() 就完全独占SysTick实在太浪费,另一方面,你也不希望其它用户仅仅因为想在SysTick_Handler中执行自己的代码就来“染指”你封装好的模块——如果有源代码还好办,如果你提供的是预先编译好的库,那用户想要往SysTick_Handler中插入自己的代码就没那么容易了(仍然可以通过特殊手段做到)。为了解决这一问题,很容易想到,继续借助weak的方式来创建一个专门的以 1ms 为间隔的事件处理函数:

//! 添加一个weak属性的默认函数实现__attribute__((weak)) void systimer_1ms_handler(void){    //! 提供了一个默认实现}void SysTick_Handler(void){    if (s_wMSCounter) {        s_wMSCounter--;    }    systimer_1ms_handler();}

通过在模块内为 systimer_1ms_handler() 提供了一个默认的函数实现,我们“复刻”了 SysTick_Handler 中断处理程序的那套技巧——用户只需要在模块外的任意地方实现一个自己的 systimer_1ms_handler() 函数,就能在“链接时刻” 实现插入自己的代码逻辑到 SysTick_Handler() 中的功能。到目前为止一切都安好。甚至为了“保证安全”,我们在使用Arm Compiler 5(也就是大家熟悉、信任和执念的armcc)时关闭了优化:

编译后通过仿真,可以看到 SysTick_Handler 对应的代码生成如下:

0x000000DA B510      PUSH     {r4,lr}    42:     if (s_wMSCounter) { 0x000000DC 481F      LDR      r0,[pc,#124]  ; @0x0000015C0x000000DE 6800      LDR      r0,[r0,#0x00]0x000000E0 B120      CBZ      r0,0x000000EC    43:         s_wMSCounter--;     44:     }     45:  0x000000E2 481E      LDR      r0,[pc,#120]  ; @0x0000015C0x000000E4 6800      LDR      r0,[r0,#0x00]0x000000E6 1E40      SUBS     r0,r0,#10x000000E8 491C      LDR      r1,[pc,#112]  ; @0x0000015C0x000000EA 6008      STR      r0,[r1,#0x00]    46:     systimer_1ms_handler(); 0x000000EC F000F868  BL.W     systimer_1ms_handler (0x000001C0)0x000000F0 BD10      POP      {r4,pc}

如果你读不懂Cortex-M的汇编,不要紧,这里的看点主要有两个地方:

  • SysTick_Handler的第一条汇编指令就是 PUSH {r4, lr},也就是将 寄存器 R4和LR 压入栈中;其中 LR 保存了中断处理程序的 “中断结束令牌”。对Cortex-M处理器来说,当一个中断处理程序结束时,只要将这一“32bit的令牌” 赋值给 PC就可以实现中断的推出;

  • SysTick_Handler的最后一条指令是 POP {r4, pc}。可以看出它实际上是和第一条指令一一对应的,最终实现的功能就是从栈中取出R4的值还给R4、取出 LR 的值赋给 PC——这就完成了从中断退出的功能。

  • 调用 systimer_1ms_handler() 时使用了 BL.W 指令,你可以先无视这里的".W"后缀,关键看 BL 的部分——这里B是Branch(跳转)的英文缩写,而 L 是 Link Register 的缩写。

  • BL指令的作用是跳转到指定的函数运行的同时,将函数的返回地址保存在LR寄存器中——当然啦,我们是function call,不是 goto,有去还要有回的嘛。


有的好奇宝宝会问,函数返回地址难道不是应该压在栈里的么?从C语言的标准模型来说是的,但Arm在这里做了一个优化,即函数的返回地址是保存在寄存器LR里的——这么做的原因是为了提高代码执行的效率。要理解这一点,请务必要在脑子里清晰的记住以下内容:

  • 栈是保存在RAM存储器里的,如果要操作栈,必然会涉及到总线操作——而进行总线操作通常会消耗2个以上的周期。一般来说保存到芯片的通用寄存器中代价就要小得多——一般可以认为不消耗或者最多消耗1个周期。从结论来说,操作 memory 比操作 寄存器页里的寄存器要“贵重”

  • 有一类函数叫做叶子函数,它的特点就是“不会继续调用其它任何函数了”。通常叶子函数会成为程序执行的热点(hot spot),也就是传说中会被重复调用、代码可能不长但却消耗很大比例CPU时间的函数——正因为这类叶子函数人小胃口大,任何一点的性能损失都会导致系统整体性能的明显下降(甚至是成倍的下降),因为,用LR来保存函数返回值(避免了栈操作)可以在大量频繁的对叶子函数的调用中避免由“贵重”的总线操作带来的性能损失

  • 有人会问,那不是叶子函数的情况怎么办呢?答案很简单,Cortex-M的架构会假设每一个函数都是叶子函数,并通过带“L”字眼的Branch指令(BL或者BLX)来完成跳转——也就是说默认先用LR保存返回地址——这是第一步,由芯片架构做出的约定。第二步,由于编译器完全掌握用户的函数间调用关系,它完全知道哪个函数是叶子函数还是普通函数,因此它可以在一个函数确实要调用别的函数时,先把LR压栈,等从目标函数返回后,再从栈中恢复原来LR中的值。其实,就拿我们这里的例子来说,如果SysTick_Handler没有调用 systimer_1s_handler(),那么它显然就是一个叶子函数,那么由于进入中断时,令牌已经保存在LR中,因此从中断处理程序中退出就只需要普通的 BX LR指令即可——通过编译我们可以轻松的验证这种说法。可以看到,由于我们屏蔽了 对systimer_1ms_handler()的调用,头尾的 PUSH和POP都消失了,取而代之的是通过 “BX lr”指令来把 LR寄存器的内容拷贝到PC中:

    42:     if (s_wMSCounter) { 0x000000DA 481F      LDR      r0,[pc,#124]  ; @0x000001580x000000DC 6800      LDR      r0,[r0,#0x00]0x000000DE B120      CBZ      r0,0x000000EA    43:         s_wMSCounter--;     44:     }     45:      46:     //systimer_1ms_handler(); 0x000000E0 481D      LDR      r0,[pc,#116]  ; @0x000001580x000000E2 6800      LDR      r0,[r0,#0x00]0x000000E4 1E40      SUBS     r0,r0,#10x000000E6 491C      LDR      r1,[pc,#112]  ; @0x000001580x000000E8 6008      STR      r0,[r1,#0x00]0x000000EA 4770      BX       lr

到目前为止,我们已经有了一个模块,并通过weak的方法为模块的使用者提供了一个毫秒级的事件源——一个周期性被调用的函数 systimer_1ms_handler()。假设因为某种原因,我们希望在默认的处理函数里加一个死循环:

#include <assert.h>__attribute__((weak))void systimer_1ms_handler(void){    assert(false);}

或者是:‍

__attribute__((weak))void systimer_1ms_handler(void){    while(1);}

为了便于观察结果,我加入了“NOP三联”:

void SysTick_Handler (void) {    if (s_wMSCounter) {        s_wMSCounter--;    }    systimer_1ms_handler();}void delay_ms(uint32_t wMillisecond){    //! 展现奇迹的 三连     __asm("nop");__asm("nop");__asm("nop");        s_wMSCounter = wMillisecond;    while( s_wMSCounter > 0 );}

编译器会就此开始它的表演,我们来看此时的代码生成:

0x000000E2 4826      LDR      r0,[pc,#152]  ; @0x0000017C0x000000E4 6800      LDR      r0,[r0,#0x00]0x000000E6 B120      CBZ      r0,0x000000F2    43:         s_wMSCounter--; ...    46:     systimer_1ms_handler();     47: }     48:      49: void delay_ms(uint32_t wMillisecond)     50: { 0x000000F2 F000F875  BL.W     systimer_1ms_handler (0x000001E0)    51:     __asm("nop");__asm("nop");__asm("nop");     52:      0x000000F6 BF00      NOP      0x000000F8 BF00      NOP      0x000000FA BF00      NOP      ...

OH,我的天哪!奇迹发生了!编译器出bug了!

  • 我们的SysTick_Handler仍然要调用函数 systimer_1ms_handler

  • 然而SysTick_Handler一头一尾的PUSH和POP却消失了!

  • 不仅如此,当从systimer_1ms_handler返回后,中断处理程序并不会结束,而是直接入侵到别的代码里了,这里通过 跟随在 BL.W 后的 “NOP”三连可以观察的非常清晰!

0x000000F2 F000F875  BL.W     systimer_1ms_handler (0x000001E0)    51:     __asm("nop");__asm("nop");__asm("nop");     52:      0x000000F6 BF00      NOP      0x000000F8 BF00      NOP      0x000000FA BF00      NOP

结论:一旦执行SysTick_Handler,由于缺乏正确的对LR的保护,中断处理程序不仅不会通过“令牌”退出(实际上保存在LR中令牌已经被 BL.W 覆盖了了),还事实上跑飞了——已经进入了别的函数的地盘。

天哪,这是"-O0"啊!

【事后分析】


这是 Arm Compiler 5 真实存在的一个bug。需要强调一点的是:在"-O0"等级下对代码进行优化并不是bug,真正造成现在这样bug的原因,我们可以进行一个合理的猜测:

  • systimer_1ms_handler 虽然被标记了 weak,但由于它跟调用它的函数SysTick_Timer() 处于同一个 “.c” 里,因此编译器觉得自己在处理SysTick_Handler时获得了它所依赖的函数的充分信息——是的,bug就在于:编译器此时忽视了weak的意义——它以为这里看到的systimer_1ms_handler的默认版本就是“全部的可能性”;

  • 由于 systimer_1ms_handler 的默认实现中使用导致函数肯定不会返回的实现,比如:"while(1);" 或是 "assert(false);" 从而让编译器确信,用户一旦在SysTick_Handler中调用了 systimer_1ms_handler 以后就再也不会回来了。

  • 基于这一考虑,编译器觉得,既然一去不复返,为啥要保护LR呢?干脆连中断退出都去掉吧。

容易注意到,编译器这里的推理都是合理的,唯一的例外就是它看漏了“weak”——当然严格说他完全看漏了也不对,因为它的确给默认版本的 systimer_1ms_handler() 追加了 weak 属性(这点可以通过你实现一个自己版本的systimer_1ms_handler() 来验证,这里就不在展开)——但它在分析当前 “.c” 文件中的函数调用关系时,的确忽略了“weak”的存在,从而导致了错误的优化推理过程。最后值得说一下的是,为啥要往默认函数里加死循环?且不说中断处理程序的默认函数都是死循环,用户可能无脑拷贝,在实际应用中可能存在以下的合理情形:

  • 用默认的函数来构造“陷阱”,也就是说,正常应用情况下,用户应该是必须要实现一个自己的版本;一旦用户漏了,就可以通过这个死循环陷阱或是assert() 抓住错误。

  • 函数可能有参数传递,而通过assert来确认参数是有效的。这种情况如果因为某种原因,传入的某个参数在编译时刻编译器就能确定这里肯定是触发了assert(),那么也会触发这一bug。

【结论】


【玄学说法1】编译器在 "-O0" 下是不会进行代码优化的

【实际情况】编译器在"-O0"下并没有许诺不进行优化,实际上它只是许诺自己所作的优化以“不影响用户调试”为前提。很多时候,它还是会做一些很基本的优化的。

【玄学说法2】在关闭优化的情况下,我的代码明明逻辑是对的,可是有时候逻辑就是不太对,好像是跑飞了,但我又没有证据……好像完全看编译器心情,有时候我随便挪挪函数的位置,好像问题就解决了。

【实际情况】编译器出bug了!而且,实际上当你无意中破坏了以下两个条件中的任意一个,都会成功回避这个bug的触发条件:

  • weak函数跟调用它的函数不放在同一个.c里(让编译器没法觉得自己获取了函数调用关系的足够信息);

  • 在weak函数里注释掉了可能会诱发死循环或是assert()的代码。

【后记】


大人,时代变了,不要继续抱着 armcc 不放了…… 它已经走到了自己生命周期的终点,已经不维护了!最后,欢迎大家尽早投入到Arm Compiler 6、IAR、GCC的怀抱……原创不易,

如果你喜欢我的思维、

如果你觉得我的文章对你有所启发或是帮助,

请“点赞、收藏、转发” 三连

欢迎订阅 裸机思维

uuid.randomuuid()回重复么_【编译器玄学研究报告】第三期——“O0” 就能逃出优化的魔爪么?...相关推荐

  1. 用java生成不重复的字符串UUID.randomUUID().toString()

    目录 0.码仙励志 1.原理 3.使用 4.去掉中间的横线 0.码仙励志 过去的靠现在忘记,将来的靠现在努力,现在才最重要 1.原理 UUID是指在一台机器上生成的数字,它保证对在同一时空中的所有机器 ...

  2. rabbitmq如何保证消息不被重复消费_如何保证消息不被重复消费

    一. 重复消息 为什么会出现消息重复?消息重复的原因有两个:1.生产时消息重复,2.消费时消息重复. 1.1 生产时消息重复 由于生产者发送消息给MQ,在MQ确认的时候出现了网络波动,生产者没有收到确 ...

  3. java后端 防重复提交_后台防止表单重复提交

    具体的做法: 1.获取用户填写用户名和密码的页面时向后台发送一次请求,这时后台会生成唯一的随机标识号,专业术语称为Token(令牌). 2.将Token发送到客户端的Form表单中,在Form表单中使 ...

  4. UUID.randomUUID()生成唯一识别码

    目录 1.UUID 的概念 2.UUID的组成 3.UUID.randomUUID()使用 1.UUID 的概念 UUID(Universally Unique Identifier):通用唯一识别码 ...

  5. uuid表示时间的部分_基于时间UUID的妙用

    1.jar包获取 https://github.com/cowtowncoder/java-uuid-generator/ com.fasterxml.uuid java-uuid-generator ...

  6. java string to uuid_在JAVA中生成UUID字符串的有效方法(不带破折号的UUID.randomUUID()。toString())...

    小编典典 最终基于UUID.java实现编写了自己的东西.请注意,我 并不是在生成UUID ,而是以我能想到的最有效的方式 生成一个 随机的32字节十六进制字符串. 实作 import java.se ...

  7. excel如何晒出重复数据_怎么筛选出excel中重复数据

    本文收集整理关于怎么筛选出excel中重复数据的相关议题,使用内容导航快速到达. 内容导航: Q1:Excel的数据怎么筛选一列中重复的数据 假如1在A2单元格,在B2单元格输入公式, =IF(COU ...

  8. 判断字段长度大于某长度_判断数据库性能只能通过count(*)?No,这些优化方案了解一下!...

    大多数用户在体验数据库时,接触到的最早的sql语句就是count(*),因此用户判断数据库性能时通常也会通过count(*)进行比较.但在执行时通常会出现一个问题:对某个表做count(*)时需对全表 ...

  9. java uuid会重复吗_记一次订单号重复的事故,快看看你的 uuid 在并发下还正确吗?...

    点击上方蓝色字体,选择"设为星标" 回复"666"获取面试宝典 去年年底的时候,我们线上出了一次事故,这个事故的表象是这样的: 系统出现了两个一模一样的订单号, ...

最新文章

  1. 【某小学生作文】《我的爸爸是名驾驶员》
  2. 【硬件基础】有源蜂鸣器与无源蜂鸣器
  3. Linux: debian,ubuntu命令行安装chrome/chromium
  4. YARN的内存和CPU配置优化
  5. socket 获取回传信息_Luat系列官方教程5:Socket代码详解
  6. 官方剧透:1.11 发版前我们偷看了 Flink 中文社区发起人的聊天记录
  7. wamp 配置 mysql_PHPWAMP配置应该如何修改,Web服务器、php、mysql的具体配置修改
  8. WF+WCF+WPF第三天-WF实现一个软件自动测试框架
  9. 大话css预编译处理(二)安装使用篇
  10. python在律师上作中的实例_基于Python的律师信息查询接口调用代码实例
  11. SCI顶级牛刊《Nature》合集PDF(2018~2020年度)
  12. 2021年8月NOC全国中小学信息技术创新与实践大赛 软件创意编程小学高年级组Python决赛题解析
  13. select XX.nextval from dual
  14. 5月25日------疯狂猜成语-----四周第七次站立会议 参会人员:杨霏,袁雪,胡潇丹,郭林林,尹亚男,赵静娜...
  15. 双u服务器装win7系统安装,u深度一键u盘装原版win7 安装系统详细使用教程
  16. 综合日语第一册第九课
  17. excel 筛选 粘贴_在筛选的Excel列表中粘贴快捷方式
  18. 『软件测试4』耗子尾汁!2021年了,你还不知道这4种白盒测试方法吗?
  19. 【x86架构】x86上的那些不明觉厉的功能
  20. stm32f103zet6驱动超声波之 USART

热门文章

  1. UVA10063 Knuth‘s Permutation【排列组合】
  2. UVA1363 LA3521 POJ2800 ZOJ2646 Joseph‘s Problem【约瑟夫环+数学】
  3. UVA10194 Football (aka Soccer)【排序】
  4. 字符串算法 —— 两字符串相同的单词
  5. linux 命令学习 —— 硬件外设管理(dmesg、lsusb)
  6. matplotlib 可视化 —— 定制 matplotlib
  7. Python debug —— 逻辑错误(四)
  8. 深度学习基础(一) —— softmax 及 logsoftmax
  9. hadoop 2.6 伪分布式的安装
  10. python自学书-大牛推荐的10本学习 Python 的好书