アラインされていないデータのアクセス

テクニカル・ノート 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-M0M0+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-M3M4M7に関しては、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_TRP1に設定されているかどうかによってコードが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++ 開発ガイド (20211)からの抜粋です。 

上記のリストを見て分かるように、__packed#pragma packに対する対応は、IARコンパイラ間でも異なります。

性能

__packed#pragma packを使うことの短所は、構造体内のアラインされていない要素へのアクセス毎にコードの使用量が増えることです。IAR C/C++ 開発ガイドには以下のように書かれています。 

「注:正しくアラインされていないオブジェクトへのアクセスによって、コードは大きくなると同時に遅くなります。そのため、もしそのような構造体メンバを頻繁にアクセスする場合は通常、パックされていないstructに正しい数値を作成し、そのstructをアクセスするやり方が望ましいでしょう。」 

Packing と unpacking

IAR C/C++ 開発ガイドで説明しているように「自分でカスタム関数を書いて構造体のパックやアンパックをすることもできます」 

この方法は最もポータビリティが高く安全ですpackunpackの欠点としては、構造体の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-M0Cortex-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用のカスタム関数を書いてください 
  • 常に正しいデータ型を使ってください。型変換によってポインタを別のデータ型に変換しないでください 

全ての製品名は、それぞれの所有者の商標または登録商標です 

 

申し訳ございませんが、弊社サイトではInternet Explorerをサポートしていません。サイトをより快適にご利用いただくために、Chrome、Edge、Firefoxなどの最新ブラウザをお使いいただきますようお願いいたします。