(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.
