(mis)Optimization

Reads/writes to registers are quite special. I may even dare to say that they are embodiment of sideeffects. In the previous example we wrote four different values to the same register. If you didn’tknow that address was a register, you may have simplified the logic to just write the final value 1 << (11 + 16) into the register.

Actually, LLVM, the compiler’s backend / optimizer, does not know we are dealing with a register andwill merge the writes thus changing the behavior of our program. Let’s check that really quick.

1. $ cargo run --release
2. (..)
3. Breakpoint 1, main () at src/07-registers/src/main.rs:9
4. 9           aux7::init();

6. (gdb) next
7. 25              *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);

9. (gdb) disassemble /m
10. Dump of assembler code for function main:
11. 7       #[entry]

13. 8       fn main() -> ! {
14. 9           aux7::init();
15. 0x08000188 <+0>:     bl      0x800019c <aux7::init>
16. 0x0800018c <+4>:     movw    r0, #4120       ; 0x1018
17. 0x08000190 <+8>:     mov.w   r1, #134217728  ; 0x8000000
18. 0x08000194 <+12>:    movt    r0, #18432      ; 0x4800

20. 10
21. 11          unsafe {
22. 12              // A magic address!
23. 13              const GPIOE_BSRR: u32 = 0x48001018;
24. 14
25. 15              // Turn on the "North" LED (red)
26. 16              *(GPIOE_BSRR as *mut u32) = 1 << 9;
27. 17
28. 18              // Turn on the "East" LED (green)
29. 19              *(GPIOE_BSRR as *mut u32) = 1 << 11;
30. 20
31. 21              // Turn off the "North" LED
32. 22              *(GPIOE_BSRR as *mut u32) = 1 << (9 + 16);
33. 23
34. 24              // Turn off the "East" LED
35. 25              *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);
36. => 0x08000198 <+16>:    str     r1, [r0, #0]

38. 26          }
39. 27
40. 28          loop {}
41. 0x0800019a <+18>:    b.n     0x800019a <main+18>

43. End of assembler dump.

The state of the LEDs didn’t change this time! The str instruction is the one that writes a valueto the register. Our debug (unoptimized) program had four of them, one for each write to theregister, but the release (optimized) program only has one.

We can check that using objdump:

1. $ # same as cargo objdump -- -d -no-show-raw-insn -print-imm-hex -source target/thumbv7em-none-eabihf/debug/registers
2. $ cargo objdump --bin registers -- -d -no-show-raw-insn -print-imm-hex -source
3. registers:      file format ELF32-arm-little

5. Disassembly of section .text:
6. main:
7. ; #[entry]
8. 8000188:       sub     sp, #0x18
9. ; aux7::init();
10. 800018a:       bl      #0xbc
11. 800018e:       str     r0, [sp, #0x14]
12. 8000190:       b       #-0x2 <main+0xa>
13. ; *(GPIOE_BSRR as *mut u32) = 1 << 9;
14. 8000192:       b       #-0x2 <main+0xc>
15. 8000194:       movw    r0, #0x1018
16. 8000198:       movt    r0, #0x4800
17. 800019c:       mov.w   r1, #0x200
18. 80001a0:       str     r1, [r0]
19. ; *(GPIOE_BSRR as *mut u32) = 1 << 11;
20. 80001a2:       b       #-0x2 <main+0x1c>
21. 80001a4:       movw    r0, #0x1018
22. 80001a8:       movt    r0, #0x4800
23. 80001ac:       mov.w   r1, #0x800
24. 80001b0:       str     r1, [r0]
25. 80001b2:       movs    r0, #0x19
26. ; *(GPIOE_BSRR as *mut u32) = 1 << (9 + 16);
27. 80001b4:       mov     r1, r0
28. 80001b6:       cmp     r0, #0x9
29. 80001b8:       str     r1, [sp, #0x10]
30. 80001ba:       bvs     #0x54 <main+0x8a>
31. 80001bc:       b       #-0x2 <main+0x36>
32. 80001be:       ldr     r0, [sp, #0x10]
33. 80001c0:       and     r1, r0, #0x1f
34. 80001c4:       movs    r2, #0x1
35. 80001c6:       lsl.w   r1, r2, r1
36. 80001ca:       lsrs    r2, r0, #0x5
37. 80001cc:       cmp     r2, #0x0
38. 80001ce:       str     r1, [sp, #0xc]
39. 80001d0:       bne     #0x4c <main+0x98>
40. 80001d2:       b       #-0x2 <main+0x4c>
41. 80001d4:       movw    r0, #0x1018
42. 80001d8:       movt    r0, #0x4800
43. 80001dc:       ldr     r1, [sp, #0xc]
44. 80001de:       str     r1, [r0]
45. 80001e0:       movs    r0, #0x1b
46. ; *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);
47. 80001e2:       mov     r2, r0
48. 80001e4:       cmp     r0, #0xb
49. 80001e6:       str     r2, [sp, #0x8]
50. 80001e8:       bvs     #0x42 <main+0xa6>
51. 80001ea:       b       #-0x2 <main+0x64>
52. 80001ec:       ldr     r0, [sp, #0x8]
53. 80001ee:       and     r1, r0, #0x1f
54. 80001f2:       movs    r2, #0x1
55. 80001f4:       lsl.w   r1, r2, r1
56. 80001f8:       lsrs    r2, r0, #0x5
57. 80001fa:       cmp     r2, #0x0
58. 80001fc:       str     r1, [sp, #0x4]
59. 80001fe:       bne     #0x3a <main+0xb4>
60. 8000200:       b       #-0x2 <main+0x7a>
61. 8000202:       movw    r0, #0x1018
62. 8000206:       movt    r0, #0x4800
63. 800020a:       ldr     r1, [sp, #0x4]
64. 800020c:       str     r1, [r0]
65. ; loop {}
66. 800020e:       b       #-0x2 <main+0x88>
67. 8000210:       b       #-0x4 <main+0x88>
68. ; *(GPIOE_BSRR as *mut u32) = 1 << (9 + 16);
69. 8000212:       movw    r0, #0x41bc
70. 8000216:       movt    r0, #0x800
71. 800021a:       bl      #0x3b28
72. 800021e:       trap
73. 8000220:       movw    r0, #0x4204
74. 8000224:       movt    r0, #0x800
75. 8000228:       bl      #0x3b1a
76. 800022c:       trap
77. ; *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);
78. 800022e:       movw    r0, #0x421c
79. 8000232:       movt    r0, #0x800
80. 8000236:       bl      #0x3b0c
81. 800023a:       trap
82. 800023c:       movw    r0, #0x4234
83. 8000240:       movt    r0, #0x800
84. 8000244:       bl      #0x3afe
85. 8000248:       trap

How do we prevent LLVM from misoptimizing our program? We use volatile operations instead of plainreads/writes:

1. #![no_main]
2. #![no_std]

4. use core::ptr;

6. #[allow(unused_imports)]
7. use aux7::{entry, iprint, iprintln};

9. #[entry]
10. fn main() -> ! {
11. aux7::init();

13. unsafe {
14. // A magic address!
15. const GPIOE_BSRR: u32 = 0x48001018;

17. // Turn on the "North" LED (red)
18. ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 9);

20. // Turn on the "East" LED (green)
21. ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 11);

23. // Turn off the "North" LED
24. ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (9 + 16));

26. // Turn off the "East" LED
27. ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (11 + 16));
28. }

30. loop {}
31. }

If we look at the disassembly of this new program compiled in release mode:

1. $ cargo objdump --bin registers --release -- -d -no-show-raw-insn -print-imm-hex -source
2. registers:      file format ELF32-arm-little

4. Disassembly of section .text:
5. main:
6. ; #[entry]
7. 8000188:       bl      #0x22
8. ; aux7::init();
9. 800018c:       movw    r0, #0x1018
10. 8000190:       mov.w   r1, #0x200
11. 8000194:       movt    r0, #0x4800
12. 8000198:       str     r1, [r0]
13. 800019a:       mov.w   r1, #0x800
14. 800019e:       str     r1, [r0]
15. 80001a0:       mov.w   r1, #0x2000000
16. 80001a4:       str     r1, [r0]
17. 80001a6:       mov.w   r1, #0x8000000
18. 80001aa:       str     r1, [r0]
19. ; loop {}
20. 80001ac:       b       #-0x4 <main+0x24>

We see that the four writes (str instructions) are preserved. If you run it (use stepi), you’llalso see that behavior of the program is preserved.