uchan note

プログラミングや電子工作の話題を書きます

「asm volatile」におけるvolatileの効果

インラインアセンブラは低レイヤプログラミングをする人にとっては有名な機能ですが,私はなぜ「volatile」を付ける必要があるのかイマイチ分かりませんでした。いままで「volatile」を付けずとも意図通り動いていたからです。しかし今回,「volatile」を付けない場合に意図しない最適化をされてしまったのでここで紹介します。

背景

LAN が無い状況でシリアル通信(RS-232)によりカーネルをダウンロードして実行したいと思いました。USB メモリを頻繁に抜き差しすると手間ですし USB ポートが劣化しますので,本格的に開発する際はオンラインでカーネルをダウンロードできる体制を整えると良いですね。UEFI のアプリからシリアル通信を行うため,COM ポートを叩くプログラムを作ろうとしてコンパイラの最適化にやられた話を書きます。

LAN があればシリアル通信なんかは使わず,UEFI + iPXE で自作 OS をネットワーク起動する に紹介した方法を使う方がもちろん良いです。

volatile 無しの最適化

COM ポートの読み書きを行うためこんな関数を作りました。シリアル通信のプログラミング自体は Serial Ports - OSDev Wiki などが参考になります。

void Out8(UINT16 addr, UINT8 x) {
  __asm__ ("outb %%al, %%dx" : : "a"(x), "d"(addr));
}

UINT8 In8(UINT16 addr) {
  UINT8 x;
  __asm__ ("inb %%dx, %%al" : "=a"(x) : "d"(addr));
  return x;
}

これらの関数は指定した IO ポートに対して 1 バイトの読み書きを行います。これらを用いて 1 文字出力を行う UARTSendChar 関数を定義します。

#define UART_THR 0x02f8
#define UART_LSR 0x02fd

int UARTEmptyTHR() {
  return (In8(UART_LSR) & 0x20) != 0;
}

void UARTSendChar(UINT8 c) {
  while (!UARTEmptyTHR());
  Out8(UART_THR, c);
}

UARTSendChar 関数は送信バッファ(Transmit Holding Register)が空になるまで待ち,空になったら 1 文字を送信バッファに書き込むという動作を意図しています。この関数の逆アセンブル結果を見てみましょう。

1274:       66 ba fd 02             mov    dx,0x2fd
1278:       ec                      in     al,dx
1279:       a8 20                   test   al,0x20  ; In8(UART_LSR) & 0x20
127b:       75 02                   jne    0x127f  ; 結果が 0 でなければ 0x127f へ
127d:       eb fe                   jmp    0x127d  ; 結果が 0 なら無限ループ
127f:       b0 25                   mov    al,0x25
1281:       66 ba f8 02             mov    dx,0x2f8
1285:       ee                      out    dx,al  ; Out8(UART_THR, c);

アセンブル結果を見ると,UARTEmptyTHR 関数により送信バッファが空か調べ,空でなかったら無限ループするというプログラムになっているではありませんか!これは全く意図した動作ではありませんね。連続して文字列を出力しようとすると 1 文字目だけ出力して後はプログラムが停止してしまいます。

volatile の効果

次のように volatile を付与してみます。

void Out8(UINT16 addr, UINT8 x) {
  __asm__ volatile("outb %%al, %%dx" : : "a"(x), "d"(addr));
}

UINT8 In8(UINT16 addr) {
  UINT8 x;
  __asm__ volatile("inb %%dx, %%al" : "=a"(x) : "d"(addr));
  return x;
}

コンパイルしてから逆アセンブルをしてみます。

1274:       66 ba fd 02             mov    dx,0x2fd
1278:       ec                      in     al,dx
1279:       a8 20                   test   al,0x20  ; In8(UART_LSR) & 0x20
127b:       74 f7                   je     0x1274  ; 結果が 0 なら 0x1274 へ
127d:       b0 25                   mov    al,0x25
127f:       66 ba f8 02             mov    dx,0x2f8
1283:       ee                      out    dx,al  ; Out8(UART_THR, c);

送信バッファが空になるまで何度も in 命令を発行し続けるプログラムになりました。やったね!

ということで,インラインアセンブラで in/out のような「実行回数が大切な命令」を使う際は volatile を付けて最適化を防ぐことが大事だということが分かりました。