アラインされていないデータのアクセス
テクニカル・ノート 210127
アーキテクチャ:
Arm
コンポーネント:
compiler
更新日:
2021/06/29 1:11
はじめに
アラインされていないデータへのアクセスが必要になることがあります。そのデータは、ネットワークやシリアルリンクから受信されてバッファに入ったものかもしれません。アラインされていないデータに安全かつポータブルな形でアクセスするには細心の注意が必要です。CPUアーキテクチャやコンパイラの最適化レベル、あるいはメモリのどの領域で作業しているかによっても、結果が異なる可能性があるからです。
このテクニカルノートでは、アラインされていないデータに関する情報をコンパイラに通知する方法と、それによってトラブルを回避する方法を示します。
解説
C言語の標準規格ISO/IEC 9899には以下のように書かれています。
“あるオブジェクトまたは不完全型を指すポインタは、別のオブジェクトや不完全型を指すポインタに変換されてしまうことがあります。もし変換の結果生じたポインタが、ポイント先の正しい型にアラインされない場合、動作は不定となります。”
基本的なデータ型
以下はuint32_tに対するポインタです。
uint32_t *data_p;
32 ビットデータ型 uint32_t にはアライメント要求があるため、 uint32_t ポインタは正しくアラインされていることが分っています。(それ以外の場合、動作は不定になります。)以下に示す関数定義で、コンパイラに対して、 out_p はアラインされた uint32_t 変数に対するポインタであると通知します。
void set_data(uint32_t *out_p, uint32_t val)
{
*out_p = val;
}
次に、以下のようなバイト配列があると仮定します。
uint8_t network_data[] = {0,1,2,3,4,5,6,7,8,9};
コンパイラを欺く
バイト配列内の“奇数”バイトを参照して uint32_t に型変換した場合、動作は不定になります。これは、変換結果のポインタがポイント先の正しい型にアラインされないからです。
data_p = (uint32_t*) &network_data[1];
ここで変換結果のdata_p ポインタを使うと動作が不定になります。
例えば、set_data 関数を data_p で呼び出すと、動作不定となります。
set_data(data_p, 1200);
キャストによってuint8_t バイトポインタである&network_data[1] のことを、アラインされたuint32_t ポインタであると知らせて、コンパイラを"欺そう"としたことになります。勿論これは正しくありません。
Cortex-M0では、 set_data の結果は UsageFault 例外となります。Cortex-M3では正常に動作します。
Cortex-M0では、 set_data で使われるSTR 命令には、アラインされたアドレスが必要です。
Cortex-M3では、STR 命令はアラインされていないアドレスでも受け付けます。
Cortex-M0、M0+、M1に関しては、Armv6-M Architecture Reference Manual に次のように書かれています。
A3.2.1 Alignment behavior
The following data accesses always generate an alignment fault:
* Non word-aligned LDR and STR
* [...]
Cortex-M3、M4、M7に関しては、ARM v7-M Architecture Reference Manual に次のように書かれています。
A3.2.1 Alignment behavior
The following data accesses support unaligned addressing, and only
generate alignment faults when the CCR.UNALIGN_TRP bit is set to 1:
* Non word-aligned LDR and STR
* [...]
つまり、Cortex-M0では、コードは常にUsageFaultを生成しますが、Cortex-M3では、CCR.UNALIGN_TRP が1に設定されているかどうかによってコードがUsageFaultを生成するかどうかが決まります。
アラインされていないアクセスの動作は不定となり、プロセッサアーキテクチャによって変わることが分かります。
以下の記述は、 Cortex-M3 Devices Generic User Guideの抜粋です。
The Cortex-M3 processor supports unaligned access only for the following
instructions: LDR, LDRT, LDRH, LDRHT, LDRSH, LDRSHT, STR, STRT, STRH, STRHT
Unaligned accesses are usually slower than aligned accesses.
In addition, some memory regions might not support unaligned accesses.
Therefore, ARM recommends that programmers ensure that accesses are aligned.
To trap accidental generation of unaligned accesses, use the UNALIGN_TRP bit
in the Configuration and Control Register.
Linux Kernel documentationには、アラインされていないメモリアクセスに関する記述もあります。
The effects of performing an unaligned memory access vary
from architecture to architecture.
- Some architectures are able to perform unaligned memory accesses
transparently, but there is usually a significant performance cost.
- Some architectures raise processor exceptions when unaligned accesses
happen.
- Some architectures are not capable of unaligned memory access, but will
silently perform a different memory access
このように、多様なアーキテクチャで使えるポータブルなアプリケーションを作成するには、アラインされていないアクセスを避ける必要があります。つまり、不定動作への依存を避けなくてはなりません。
コンパイラから助けを得る
では、コンパイラで、アラインされていないアクセスを避けるにはどうすればよいでしょうか?その答えは、そのデータがアラインされていないとコンパイラに伝えることです。これには#pragma packや__packedを使います。あるいはIAR C/C++ 開発ガイドで述べているように「自分でカスタム関数を書いて構造体のパックやアンパックをします。」
__packedを使う
前出の例を修正して__packed データ型属性を使うことで、データがアラインされていない可能性があることをコンパイラに伝えることができます。
void set_unaligned_data(uint32_t __packed *out_p, uint32_t val)
{
*out_p = val;
}
上記のコードを使って、uint32_t out_p ポインタがアラインされていない可能性があるとコンパイラに伝えました。これを受けてコンパイラは調整を行います。
もし、アラインされていない __packed ポインタを使って元のset_data 関数を呼び出そうとすると、コンパイラから以下のような有用なエラーメッセージが示されます。
Error[Pe167]: argument of type "uint32_t __packed *" is incompatible with parameter of type "uint32_t *"
これでset_unaligned_data 関数はCortex-M0でもCortex-M3でも問題なく動作します。Cortex-M0では、STR 命令はもう使われません。しかし、Cortex-M3では、驚かれるかもしれませんが、データがアラインされていないことをコンパイラに伝えた後でもSTR 命令がまだ使われます。これは、Cortex-M3ハードウェアがある種のアラインされていないアクセスに対応していることを、コンパイラが「知っている」ためです。ハードウェアが対応可能なアラインされていないアクセスを避けるためには、 --no_unaligned_access コンパイラオプションを使ってください。
Armのドキュメントにあるように、「偶然に生成された(あらゆる)アラインされていないアクセスを見つけ出して「捉える」ためには、 UNALIGN_TRP ビットを使ってください。」
構造体
#pragma pack を使う
構造体に関しては、 #pragma pack を用いると構造体をよりタイトにレイアウトできます。また、このデータ型属性でも、アラインされていないデータが構造体に含まれている可能性があることがコンパイラに伝えられます。パック構造体型を使った場合は、コンパイラは、データがアラインされていないかもしれないことを認識して、適切に調整を行います。以下に例を挙げます。
#pragma pack(1)
typedef struct my_packed_struct_s {
uint8_t byte1;
uint32_t val1;
uint8_t byte2;
uint32_t val2;
} my_packed_struct_t;
#pragma pack()
my_packed_struct_t *struct_p = (my_packed_struct_t*) &network_data[0];
void set_s_data(my_packed_struct_t *out_p, uint32_t v1, uint32_t v2)
{
out_p->val1 = v1;
out_p->val2 = v2;
}
set_s_data 関数は my_packed_struct_t 型を使っているので、データがアラインされていない可能性があることがコンパイラに伝わります(my_packed_struct_tの #pragma pack ディレクティブによって)。それに従って、コンパイラは適切に調整を行います。
もし、パック構造体内のアラインされていない可能性のあるデータに対するポインタを作ろうとすると、コンパイラは、以下のような有用な警告を出します。
uint32_t *p = &out_p->val1;
Warning[Pa039]: use of address of unaligned structure member
ポータビリティ
異なるアーキテクチャやコンパイラ間でのポータビリティがアプリケーションにとって真に必要である場合は、IAR独自のpragmaディレクティブおよびデータ型属性のサポートリストを検討してみてください。
以下のリストはIAR C/C++ 開発ガイド (2021年1月)からの抜粋です。
上記のリストを見て分かるように、 __packed と#pragma pack に対する対応は、IARコンパイラ間でも異なります。
性能
__packed や#pragma pack を使うことの短所は、構造体内のアラインされていない要素へのアクセス毎にコードの使用量が増えることです。IAR C/C++ 開発ガイドには以下のように書かれています。
「注:正しくアラインされていないオブジェクトへのアクセスによって、コードは大きくなると同時に遅くなります。そのため、もしそのような構造体メンバを頻繁にアクセスする場合は、通常、パックされていないstructに正しい数値を作成し、そのstructをアクセスするやり方が望ましいでしょう。」
Packing と unpacking
IAR C/C++ 開発ガイド で説明しているように「自分でカスタム関数を書いて構造体のパックやアンパックをすることもできます」。
この方法は最もポータビリティが高く安全です。packやunpackの欠点としては、構造体のpackデータとunpackアンパックデータ用に2つのビューが必要となることが挙げられます。
上記の例の続きで説明します。packおよびunpack関数は、通常、以下のように書かれています (my_struct_t はアラインされたデータを持つ通常の構造体)。
void unpack_data(const uint8_t *unaligned_data_p, my_struct_t *struct_p);
void pack_data(uint8_t *unaligned_data_p, const my_struct_t *struct_p);
サンプルプロジェクト
サンプルプロジェクト 2021-01-22_unaligned_8509.zip には、 __packed、#pragma packおよびパックとアンパックのカスタム関数のサンプルがあります。さらに、Cortex-M0やCortex-M3では、アラインされていないアクセスが最終的にUsageFault_Handlerとなることも示されます。
サンプルプロジェクトでは、C-SPYシミュレータ・デバッガのドライバと View(表示)>Memory(メモリ) ウィンドウを使って network_data 変数を調べてみてください。C-SPYシミュレータは、アラインされていないアクセスの検出ツールとしても役立ちます。サンプルコードを実行すると、以下のように、アーキテクチャ毎に異なる有用なデバッガの警告が表示されます。
MSP430: Warning: A word access on odd address
RL78: Word write access at odd address
RISC-V: Misaligned word data access
まとめ
アドレスのアラインメントを行えない場合は、型、つまり#pragma packまたは__packedを使って、アラインされていないことを示す必要があります。しかし、この方法は実際に必要な場合を除き推奨できません。できうる限りアラインされているアドレスを使ってください。
ポータビリティや性能の観点から、アラインされていないメモリのアクセスは避けてください。
- コンパイラの力を借りるには、#pragma pack ディレクティブまたは__packedデータ型属性を使って、アラインされていないデータの存在をコンパイラに伝えてください。
- あるいは、構造体のpackおよびunpack用のカスタム関数を書いてください。
- 常に正しいデータ型を使ってください。型変換によってポインタを別のデータ型に変換しないでください。
全ての製品名は、それぞれの所有者の商標または登録商標です