(誤)最適化
レジスタの読み書きはかなり特殊です。副作用を体現したものだと言ってしまってもよいくらいです。前の例では、同じレジスタに 4 つの異なる値を書き込みました。そのアドレスがレジスタだと知らなければ、ロジックを単純化して最後の値 1 << (11 + 16) だけを書き込むようにしてしまうかもしれません。
実際には、コンパイラのバックエンド / オプティマイザである LLVM は、こちらがレジスタを扱っていることを知りません。そのため、これらの書き込みをまとめてしまい、結果としてプログラムの動作を変えてしまいます。手早く確認してみましょう。
$ cargo run --release
(..)
Breakpoint 1, registers::__cortex_m_rt_main_trampoline () at src/07-registers/src/main.rs:7
7 #[entry]
(gdb) step
registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:9
9 aux7::init();
(gdb) next
25 *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);
(gdb) disassemble /m
Dump of assembler code for function _ZN9registers18__cortex_m_rt_main17h45b1ef53e18aa8d0E:
8 fn main() -> ! {
0x08000248 <+0>: push {r7, lr}
0x0800024a <+2>: mov r7, sp
9 aux7::init();
0x0800024c <+4>: bl 0x8000260 <aux7::init>
0x08000250 <+8>: movw r0, #4120 ; 0x1018
0x08000254 <+12>: mov.w r1, #134217728 ; 0x8000000
0x08000258 <+16>: movt r0, #18432 ; 0x4800
10
11 unsafe {
12 // マジックアドレス!
13 const GPIOE_BSRR: u32 = 0x48001018;
14
15 // "North" LED(赤)を点灯
16 *(GPIOE_BSRR as *mut u32) = 1 << 9;
17
18 // "East" LED(緑)を点灯
19 *(GPIOE_BSRR as *mut u32) = 1 << 11;
20
21 // "North" LED を消灯
22 *(GPIOE_BSRR as *mut u32) = 1 << (9 + 16);
23
24 // "East" LED を消灯
25 *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);
=> 0x0800025c <+20>: str r1, [r0, #0]
0x0800025e <+22>: b.n 0x800025e <registers::__cortex_m_rt_main+22>
End of assembler dump.
今回は LED の状態が変化しませんでした! str 命令は、レジスタに値を書き込む命令です。debug(非最適化)プログラムにはそれが 4 つあり、レジスタへの各書き込みごとに 1 つずつ対応していましたが、release(最適化)プログラムには 1 つしかありません。
これは objdump を使って確認でき、その出力を out.asm に保存できます。
# cargo objdump -- -d --no-show-raw-insn --print-imm-hex --source target/thumbv7em-none-eabihf/debug/registers と同じ
cargo objdump --bin registers -- -d --no-show-raw-insn --print-imm-hex --source > debug.txt
次に、debug.txt の中で main を探すと、4 つの str 命令があることが分かります。
080001ec <main>:
; #[entry]
80001ec: push {r7, lr}
80001ee: mov r7, sp
80001f0: bl #0x2
80001f4: trap
080001f6 <registers::__cortex_m_rt_main::hc2e3436fa38cd6f2>:
; fn main() -> ! {
80001f6: push {r7, lr}
80001f8: mov r7, sp
; aux7::init();
80001fa: bl #0x3e
80001fe: b #-0x2 <registers::__cortex_m_rt_main::hc2e3436fa38cd6f2+0xa>
; *(GPIOE_BSRR as *mut u32) = 1 << 9;
8000200: movw r0, #0x2640
8000204: movt r0, #0x800
8000208: ldr r0, [r0]
800020a: movw r1, #0x1018
800020e: movt r1, #0x4800
8000212: str r0, [r1]
; *(GPIOE_BSRR as *mut u32) = 1 << 11;
8000214: movw r0, #0x2648
8000218: movt r0, #0x800
800021c: ldr r0, [r0]
800021e: str r0, [r1]
; *(GPIOE_BSRR as *mut u32) = 1 << (9 + 16);
8000220: movw r0, #0x2650
8000224: movt r0, #0x800
8000228: ldr r0, [r0]
800022a: str r0, [r1]
; *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);
800022c: movw r0, #0x2638
8000230: movt r0, #0x800
8000234: ldr r0, [r0]
8000236: str r0, [r1]
; loop {}
8000238: b #-0x2 <registers::__cortex_m_rt_main::hc2e3436fa38cd6f2+0x44>
800023a: b #-0x4 <registers::__cortex_m_rt_main::hc2e3436fa38cd6f2+0x44>
(..)
LLVM がプログラムを誤って最適化しないようにするにはどうすればよいのでしょうか? 通常の読み書きの代わりに volatile 操作を使います。
#![no_main]
#![no_std]
use core::ptr;
#[allow(unused_imports)]
use aux7::entry;
#[entry]
fn main() -> ! {
aux7::init();
unsafe {
// マジックアドレス!
const GPIOE_BSRR: u32 = 0x48001018;
// "North" LED(赤)を点灯
ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 9);
// "East" LED(緑)を点灯
ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 11);
// "North" LED を消灯
ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (9 + 16));
// "East" LED を消灯
ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (11 + 16));
}
loop {}
}
--release モードで release.txt を生成します。
cargo objdump --release --bin registers -- -d --no-show-raw-insn --print-imm-hex --source > release.txt
次に release.txt の中で main ルーチンを探すと、4 つの str 命令があることが分かります。
0800023e <main>:
; #[entry]
800023e: push {r7, lr}
8000240: mov r7, sp
8000242: bl #0x2
8000246: trap
08000248 <registers::__cortex_m_rt_main::h45b1ef53e18aa8d0>:
; fn main() -> ! {
8000248: push {r7, lr}
800024a: mov r7, sp
; aux7::init();
800024c: bl #0x22
8000250: movw r0, #0x1018
8000254: mov.w r1, #0x200
8000258: movt r0, #0x4800
; intrinsics::volatile_store(dst, src);
800025c: str r1, [r0]
800025e: mov.w r1, #0x800
8000262: str r1, [r0]
8000264: mov.w r1, #0x2000000
8000268: str r1, [r0]
800026a: mov.w r1, #0x8000000
800026e: str r1, [r0]
8000270: b #-0x4 <registers::__cortex_m_rt_main::h45b1ef53e18aa8d0+0x28>
(..)
4 回の書き込み(str 命令)が保持されていることが分かります。gdb を使って実行すると、期待どおりの動作になることも確認できます。
注: 最後の
nextはloop {}を延々と実行するので、Ctrl-cを使って(gdb)プロンプトに戻ってください。
$ cargo run --release
(..)
Breakpoint 1, registers::__cortex_m_rt_main_trampoline () at src/07-registers/src/main.rs:9
9 #[entry]
(gdb) step
registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:11
11 aux7::init();
(gdb) next
18 ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 9);
(gdb) next
21 ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 11);
(gdb) next
24 ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (9 + 16));
(gdb) next
27 ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (11 + 16));
(gdb) next
^C
Program received signal SIGINT, Interrupt.
0x08000270 in registers::__cortex_m_rt_main ()
at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:1124
1124 intrinsics::volatile_store(dst, src);
(gdb)