C 言語の union に関して
C言語には、union
という機能があり、日本語では共用体と翻訳されている。
C言語を始めたころは、共用体という名称から実態に対する別名(alias)と勝手に思い込んでいたのだが、
実際は、"異なるシンボルに対して同じメモリーアドレス値で初期化する機能”であり、それ以上でもそれ
以下でも無いのだが、名称から誤認するのは決して私だけでは無いと思うので、注意事項として記録に残す。
特に広域最適化などを行うと、特定シンボルに対する操作が無意味と判断されコード自体が生成されない
事もあるので、共用体の機能とすこぶる相性の悪い事象も発生する。
つまり、unionで共通アドレスに割り当てられるシンボルは、シンボル毎に独立した変数(シンボル)として
扱われ、そして独自に最適化(例えばレジスターに保存した状態でメモリーには変更されない)されるので、
希望する結果と異なる場合が存在してしまう。
ここでは、union を使って、データの部分抽出(入力用シンボルと読み出し用シンボルを変える)をすること
を考える。 本来は、C言語の仕様では保証・許可(未定義)されていない操作
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
typedef union PHANTOM {
volatile long long ll;
volatile unsigned int ui[sizeof(long long)/sizeof(int)];
volatile unsigned short us[sizeof(long long)/sizeof(short)];
volatile unsigned char uc[sizeof(long long)/sizeof(char)];
} PHANTOM;
long long CreatingRandomNumbers(){
return (PHANTOM){.us[3]=rand(), .us[2]=rand(), .us[1]=rand(), .us[0]=rand()}.ll;
}
int main(){ srand((unsigned int)time(NULL));
int e = 5;
long long b = CreatingRandomNumbers();
printf( " (PHANTOM).ll = 0x%16.16llX\n", b );
printf( " (PHANTOM).uc[%d] = 0x%4.4X\n", e, ((PHANTOM)b).uc[e] );
////////////////////////////////////////////////////////////////////
// .ll = 0x1122334455667788 のメモリー上の割り当て
// x86系は リトルエンディアン 【little endian】なので
// .uc[0] - .uc[7]: { 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11 }
b = CreatingRandomNumbers(); // 変更が正しく反映されるかの確認
printf( " (PHANTOM).ll = 0x%16.16llX\n", b );
printf( " (PHANTOM).uc[%d] = 0x%4.4X\n", e, ((PHANTOM)b).uc[e] );
PHANTOM c = (PHANTOM){ .ll = CreatingRandomNumbers() };
printf( " (PHANTOM).ll = 0x%16.16llX\n", c.ll );
printf( " (PHANTOM).uc[%d] = 0x%4.4X\n", e, c.uc[e] );
return 0;
}
これは、x86系(リトルエンディアン)の環境で、別定義 された変数から、特定の場所にあるバイト
データを抽出したいときの操作方法。
D:\UNION>
D:\UNION>gcc -O3 -Wall -W -o union union.c
D:\UNION>union.exe
(PHANTOM).ll = 0x77FC0FFD75C920AD
(PHANTOM).uc[5] = 0x000F
(PHANTOM).ll = 0x7DEA643C278B529D
(PHANTOM).uc[5] = 0x0064
(PHANTOM).ll = 0x5FF4655138BC7B10
(PHANTOM).uc[5] = 0x0065
データーアドレスの、6バイト目の符号無し8ビットデータを抽出し、その値を表示している。
各変数を volatile 指定して、外部書き換えの可能性があるので最適化から外すようにコンパイラ
に指示している。 当然、最適化の対象から外れるので使い方によっては、性能低下をもたらす場合も
出てくるが、通常の利用ではそこまでの考慮は不要かも。(普通はキャシュ止り,実メモリアクセス無し)
まずは、C言語からそのアセンブラコードを出力させ、その挙動を確認する。
コンパイル時に、 -S オプションを付けることで、アセンブラファイル union.S が作成される
D:\UNION>
D:\UNION>gcc -O3 -Wall -W -S -o union.S union.c
D:\UNION>
重要な部分を抽出すると以下の様になる。
.LC0:
.ascii " (PHANTOM).ll = 0x%16.16llX\12\0"
.LC1:
.ascii " (PHANTOM).uc[%d] = 0x%4.4X\12\0"
leaq .LC0(%rip), %rdi
leaq .LC1(%rip), %rsi
call CreatingRandomNumbers
movq %rdi, %rcx # " (PHANTOM).ll = 0x%16.16llX\12\0"
movq %rax, %rbx
movq %rax, %rdx
call printf
movq %rbx, 48(%rsp)
movzbl 53(%rsp), %r8d # ((PHANTOM)b).uc[5]
movq %rsi, %rcx # " (PHANTOM).uc[%d] = 0x%4.4X\12\0"
movl $5, %edx # 変数 e は 5 から変更されないので即値に置き換え
call printf
call CreatingRandomNumbers
movq %rdi, %rcx # " (PHANTOM).ll = 0x%16.16llX\12\0"
movq %rax, %rbx
movq %rax, %rdx
call printf
movq %rbx, 56(%rsp)
movzbl 61(%rsp), %r8d # ((PHANTOM)b).uc[5]
movq %rsi, %rcx # " (PHANTOM).uc[%d] = 0x%4.4X\12\0"
movl $5, %edx # 5
call printf
call CreatingRandomNumbers
movq %rdi, %rcx # " (PHANTOM).ll = 0x%16.16llX\12\0"
movq %rax, 40(%rsp)
movq 40(%rsp), %rdx # .ll
call printf
movzbl 45(%rsp), %r8d # c.uc[5]
movl $5, %edx # 5
movq %rsi, %rcx # " (PHANTOM).uc[%d] = 0x%4.4X\12\0"
call printf
● printf は、シェアードライブラリ(fprintf)を呼び出すための内部関数
● シンボル (PHANTOM).ll のデータは、rax レジスタが保持している
● シンボル (PHANTOM).ll と (PHANTOM).uc のメモリー上のベースアドレスは共通
rax レジスタに格納されている値をメモリーに書き出(フラシュ)し、該当データー部を
( %rax -> %rbx -> 48(%rsp)、%rax -> %rbx -> 56(%rsp)、%rax -> 40(%rsp) )
r8d レジスタに読みだすことで、.ll と .uc で不整合が生じないように処理されている
最適化オプションを指定して最適化すると、レジスター間での処理をするコードが作成されるが、
実際のメモリー(スタック領域)にデータを書き出す部分が、最適化処理がされていないコード
volatile 指定して非最適化コードを作成している。 (常にこの様な生成がされる保証は無い)
一度、メモリーに値を書き出し、再度必要な部分を読みだすことで一貫性の確保が成される
movzbl 45(%rsp), %r8d
は、x86-64アーキテクチャのアセンブリ言語の命令で、特定のメモリアドレスから 1バイト
のデータを読み取り、それを32ビットのレジスタ %r8d に符号なし拡張して格納します。
具体的には:
movzbl: メモリからバイトを読み取り、それを符号なし拡張してレジスタに格納する
ための命令です。
45(%rsp): %rspをベースとした相対アドレッシング。スタックポインタ (%rsp) から
45バイト下がった位置のメモリを指します。
%r8d: 32ビットのレジスタ %r8の下位32ビット部分。%r8は64ビットのレジスタ
であり、%r8d はその下位32ビット部分を指します。
この命令全体の動作は、スタック上の特定のメモリアドレスから1バイトのデータを読み取り、
それを32ビットのレジスタ %r8dに符号なしで格納することです。符号なし拡張が行われるため、
%r8dの上位ビットはゼロで埋められます。
◎ エンディアン処理命令としてなら、BSWAP (4バイトオーダ一括変更命令)も非常に有効
(C言語の記述を考慮、コンパイラに最適化処理時 bswap %r8d の様なコードを出させる)
64ビットレジスタ使用時は、8文字分(8バイト)の入れ替えが1命令で完了、効果は絶大
see also __builtin_bswap32(value), __builtin_bswap64(value)
本ソフトウェアは、著作権者およびコントリビューターによって「現状のまま」提供
されており、明示黙示を問わず、商業的な使用可能性、および 特定の目的に対する
適合性に関する暗黙の保証も含め、またそれに限定されない、いかなる保証もない。
著作権者もコントリビューターも、事由のいかんを問わず、損害発生の原因いかんを
問わず、かつ責任の根拠が契約であるか厳格責任であるか(過失その他の)不法行為
であるかを問わず、仮にそのような損害が発生する可能性を知らされていたとしても、
本ソフトウェアの使用によって発生した(代替品または代用サービスの調達、使用の
喪失、データの喪失、利益の喪失、業務の中断も含め、またそれに限定されない)
直接損害、間接損害、偶発的な損害、特別損害、懲罰的損害、又は結果損害について、
一切責任を負わないものとする。
但し下記の制限が課される。
1、人命に危害を与える機器及びソフトウエアでの使用を禁じる。
2、前記1に記載物に連携する、機器、ソフトウエア及びシステムでの使用を禁じる。
コンピュータで実行するコードを生成する方法の言語として C99 は非常に優れており
おかしな拡張などはせず、現行機能の踏襲と吟味された有効機能、例えば、
・ 2進数記述は必須機能
・ 3桁毎にカンマを入れる数字出力機能
の追加等に注力してもらいたいと切に願っている。
おまけ
# return (PHANTOM){ .us[3]=rand(), .us[2]=rand(),
# .us[1]=rand(), .us[0]=rand() }.ll;
CreatingRandomNumbers:
subq $56, %rsp
.seh_stackalloc 56
.seh_endprologue
call rand
movw %ax, 40(%rsp)
call rand
movw %ax, 42(%rsp)
call rand
movw %ax, 44(%rsp)
call rand
movw %ax, 46(%rsp)
movq 40(%rsp), %rax
addq $56, %rsp
ret