+===========+ |P/ECE研究室| +===========+ P/ECE研究記録 2004年 ==================== * Sun Dec 5 16:00:00 JST 2004 Naoyuki Sawa - ADPCMの仕組み#5 今回も絵が多いのでHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/adpcm/adpcm5.html * Sat Dec 4 14:45:00 JST 2004 Naoyuki Sawa - ADPCMの仕組み#4 今回も絵が多いのでHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/adpcm/adpcm4.html * Fri Dec 3 00:00:00 JST 2004 Naoyuki Sawa - ADPCMの仕組み#3 今回も絵が多いのでHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/adpcm/adpcm3.html * Wed Dec 1 00:00:00 JST 2004 Naoyuki Sawa - ADPCMの仕組み#2 今回も絵が多いのでHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/adpcm/adpcm2.html * Tue Nov 30 06:00:00 JST 2004 Naoyuki Sawa - ADPCMの仕組み#1 今回は絵が多いのでHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/adpcm/adpcm1.html * Sat Nov 7 09:00:00 JST 2004 Naoyuki Sawa - 構造体パックに問題あり#2 前回、P/ECE開発環境では、構造体パックのための__attribute__((packed))が正しく動作しない、というところまでお話しました。 具体的には、構造体サイズは正しくパックされるのだけれど、初期化済み構造体の内容がパックされていない、という症状です。 今回は、その原因を探ってみます。 結論だけ言うと一言で終わってしまうのですが、それでは面白くないので(^^;、僕が調査したときの手順を追って説明してみます。 長いですけれど、おつきあいください。 ■■■■■■■■■■■■■■■■ ■コンパイラ出力を比較してみる■ ■■■■■■■■■■■■■■■■ さてそれでは、構造体アライメントあり(=構造体パック無し)の場合と構造体アライメント無し(=構造体パックあり)の場合とで、 C言語ソースがどのようにコンパイルされ、どのようなアセンブラコードが生成されているのか見てみます。 C言語ソースの内容は、前回の後半■P/ECEの場合〜問題発生!!■を参照してください。 まずは、後半部分にある、pceAppProc()関数がどのようにコンパイルされているのか見てみます。 最初に、__attribute__((packed))を指定しない、構造体アライメントありの場合。 [構造体アライメントあり] ; void pceAppProc(int count) ; { ;   int i; ;   unsigned char* p; ; ;   memset(vbuff, 0, sizeof vbuff); ;   pceFontSetType(2); ;   pceFontSetPos(0, 0); ; ;   p = (unsigned char*)&cid; ;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO)); ;   for(i = 0; i < sizeof(MMC_CID_INFO); i++) { ;     pceFontPrintf("%02x ", p[i]); ;   } ; ;   pceLCDTrans(); ; } ; ; の部分が、次のようにコンパイルされます。 pceAppProc:     pushn %r2     xsub %sp,%sp,8     xld.w %r12,vbuff     ld.w %r13,0x0     xld.w %r14,0x00002c00     xcall memset     xld.w %r12,0x00000002     xcall pceFontSetType     ld.w %r12,0x0     ld.w %r13,%r12     xcall pceFontSetPos     xld.w %r10,__LC0     xld.w [%sp],%r10     xld.w %r10,0x00000014    ; 0x00000014(20) = sizeof(MMC_CID_INFO)     xld.w [%sp+4],%r10     xcall pceFontPrintf     xld.w %r2,__LC1     xld.w %r0,cid     xadd %r1,%r0,15 __L6:     xld.w [%sp],%r2     xld.ub %r10,[%r0]     xld.w [%sp+4],%r10     xcall pceFontPrintf     xadd %r0,%r0,1     cmp %r0,%r1     xjrule __L6     xcall pceLCDTrans     xadd %sp,%sp,8     popn %r2     ret 次に、__attribute__((packed))を指定した、構造体アライメント無しの場合。 [構造体アライメント無し] ; void pceAppProc(int count) ; { ;   int i; ;   unsigned char* p; ; ;   memset(vbuff, 0, sizeof vbuff); ;   pceFontSetType(2); ;   pceFontSetPos(0, 0); ; ;   p = (unsigned char*)&cid; ;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO)); ;   for(i = 0; i < sizeof(MMC_CID_INFO); i++) { ;     pceFontPrintf("%02x ", p[i]); ;   } ; ;   pceLCDTrans(); ; } ; ; の部分が、次のようにコンパイルされます。 pceAppProc:     pushn %r2     xsub %sp,%sp,8     xld.w %r12,vbuff     ld.w %r13,0x0     xld.w %r14,0x00002c00     xcall memset     xld.w %r12,0x00000002     xcall pceFontSetType     ld.w %r12,0x0     ld.w %r13,%r12     xcall pceFontSetPos     xld.w %r10,__LC0     xld.w [%sp],%r10     xld.w %r10,0x00000010    ; 0x00000010(16) = sizeof(MMC_CID_INFO)     xld.w [%sp+4],%r10     xcall pceFontPrintf     xld.w %r2,__LC1     xld.w %r0,cid     xadd %r1,%r0,15 __L6:     xld.w [%sp],%r2     xld.ub %r10,[%r0]     xld.w [%sp+4],%r10     xcall pceFontPrintf     xadd %r0,%r0,1     cmp %r0,%r1     xjrule __L6     xcall pceLCDTrans     xadd %sp,%sp,8     popn %r2     ret sizeof(MMC_CID_INFO)の値が、構造体アライメントありの場合はパディング込みで20バイト、構造体アライメント無しの場合は16バイトになっています。 P/ECE開発環境のCコンパイラは、構造体アライメントありの場合と無しの場合とで、構造体サイズの違いをきちんと処理できていることがわかります。 構造体サイズが変化すること以外は、構造体アライメントありの場合も無しの場合も、まったく同じコンパイル結果です。 問題ありません。 それでは、前半部分の、初期化済み構造体の定義がどのようにコンパイルされているか見てみます。 最初に、__attribute__((packed))を指定しない、構造体アライメントありの場合。 [構造体アライメントあり] ; //CID(Card IDentification number) カードIDレジスタ情報構造体 ; typedef struct tag_MMC_CID_INFO ; { ;   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit) ;   unsigned short usOID;       // + 2, 2: OEM/Application ID(16bit) ;   unsigned char  ucPNM[6];      // + 4, 6: Product name(48bit) ;   unsigned char  ucPRV;       // +10, 1: Product revision(8bit) ;   unsigned long  ulPSN;       // +12, 4: Product serial number(32bit) ;   unsigned char  ucMDT;       // +16, 1: Manufacturing date(8bit) ;   unsigned char  ucCRC;       // +17, 1: 7-bit CRC checksum(7bit) ; } __attribute__((packed)) MMC_CID_INFO; // =20 ; ; MMC_CID_INFO cid = { ;   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit) ;   0xffff,                 // unsigned short usOID;  // + 2, 2: OEM/Application ID(16bit) ;   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 4, 6: Product name(48bit) ;   0xff,                  // unsigned char  ucPRV;  // +10, 1: Product revision(8bit) ;   0xffffffff,               // unsigned long  ulPSN;  // +12, 4: Product serial number(32bit) ;   0xff,                  // unsigned char  ucMDT;  // +16, 1: Manufacturing date(8bit) ;   0xff,                  // unsigned char  ucCRC;  // +17, 1: 7-bit CRC checksum(7bit) ; }; ; ; の部分が、次のようにコンパイルされます。     .align 2 cid:     .byte  255     .space 1     .half  65535     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .space 1     .word  -1     .byte  255     .byte  255     .space 2 次に、__attribute__((packed))を指定した、構造体アライメント無しの場合。 [構造体アライメント無し] ; //CID(Card IDentification number) カードIDレジスタ情報構造体 ; typedef struct tag_MMC_CID_INFO ; { ;   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit) ;   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit) ;   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit) ;   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit) ;   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit) ;   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit) ;   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) ; } __attribute__((packed)) MMC_CID_INFO; // =16 ; ; MMC_CID_INFO cid = { ;   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit) ;   0xffff,                 // unsigned short usOID;  // + 1, 2: OEM/Application ID(16bit) ;   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 3, 6: Product name(48bit) ;   0xff,                  // unsigned char  ucPRV;  // + 9, 1: Product revision(8bit) ;   0xffffffff,               // unsigned long  ulPSN;  // +10, 4: Product serial number(32bit) ;   0xff,                  // unsigned char  ucMDT;  // +14, 1: Manufacturing date(8bit) ;   0xff,                  // unsigned char  ucCRC;  // +15, 1: 7-bit CRC checksum(7bit) ; }; ; ; の部分が、次のようにコンパイルされます。     .align 2 cid:     .byte  255     .half  65535     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .word  -1     .byte  255     .byte  255 おや?一見問題ないように見えます。 わかりやすいように、コメントに付けてみましょう。 [構造体アライメントあり]     .align 2    ; 次の位置が確実に(1<<2)=4の倍数アドレスから始まるようにする。 cid:     .byte  255   ; // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)     .space 1    ; // (パディング)       // + 1, 1:     .half  65535  ; // unsigned short usOID;  // + 2, 2: OEM/Application ID(16bit)     .byte  255   ; // unsigned char  ucPNM[6]; // + 4, 6: Product name(48bit)     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ; // unsigned char  ucPRV;  // +10, 1: Product revision(8bit)     .space 1    ; // (パディング)       // +11, 1:     .word  -1    ; // unsigned long  ulPSN;  // +12, 4: Product serial number(32bit)     .byte  255   ; // unsigned char  ucMDT;  // +16, 1: Manufacturing date(8bit)     .byte  255   ; // unsigned char  ucCRC;  // +17, 1: 7-bit CRC checksum(7bit)     .space 2    ; // (パディング)       // +18, 2: 構造体アライメントありの場合については、完全に問題ありません。 前回行ったP/ECE上でのテストでも、期待通りの実行結果となっていました。 構造体アライメントありの場合については、もう検討する必要はなさそうです。 問題は、構造体アライメント無しの場合です。 [構造体アライメント無し]     .align 2    ; 次の位置が確実に(1<<2)=4の倍数アドレスから始まるようにする。 cid:     .byte  255   ; // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)     .half  65535  ; // unsigned short usOID;  // + 1, 2: OEM/Application ID(16bit)     .byte  255   ; // unsigned char  ucPNM[6]; // + 3, 6: Product name(48bit)     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ; // unsigned char  ucPRV;  // + 9, 1: Product revision(8bit)     .word  -1    ; // unsigned long  ulPSN;  // +10, 4: Product serial number(32bit)     .byte  255   ; // unsigned char  ucMDT;  // +14, 1: Manufacturing date(8bit)     .byte  255   ; // unsigned char  ucCRC;  // +15, 1: 7-bit CRC checksum(7bit) コンパイル結果のアセンブラコードを見る限りは、問題無さそうに見えます。 が、実は、上のコメントのようなメモリ配置にはならないのです。 『アセンブラにも暗黙のアライメント機能がある』というのがその理由です。 ■■■■■■■■■■■■■■■■ ■アセンブラのアライメント機能■ ■■■■■■■■■■■■■■■■ P/ECE開発環境に入っている、「S1C33 Family Cコンパイラパッケージ Ver.4」のマニュアルを開いてみてください。 (C:\usr\PIECE\docs\datasheet\EPSON\S5U1C33000C_J.pdf) マニュアル171ページ(PDFでは187ページ目)、「11.8.6 データ定義擬似命令」のところに、次のような記述があります。 .word擬似命令 ●注意事項 ・定義したデータは直前に.align命令がない限り、ワード境界アドレスから配置されます。  現在位置がワード境界アドレス以外の場合、データを配置するワード境界アドレスまでの間は0x00が設定されます。 .half擬似命令 ●注意事項 ・定義したデータは直前に.align命令がない限り、ハーフワード境界アドレスから配置されます。  現在位置が奇数アドレスの場合、現在位置には0x00が設定されます。 これです!! つまり、コンパイル結果のアセンブラコードがas33.exeによってアセンブルされると、次のようなメモリ配置になっていたのです。 [構造体アライメント無し]     .align 2    ; 次の位置が確実に(1<<2)=4の倍数アドレスから始まるようにする。 cid:     .byte  255   ; // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)     (0x00)      ; // (パディング)       // + 1, 1:     .half  65535  ; // unsigned short usOID;  // + 2, 2: OEM/Application ID(16bit)     .byte  255   ; // unsigned char  ucPNM[6]; // + 4, 6: Product name(48bit)     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ; // unsigned char  ucPRV;  // +10, 1: Product revision(8bit)     (0x00)      ; // (パディング)       // +11, 1:     .word  -1    ; // unsigned long  ulPSN;  // +12, 4: Product serial number(32bit)     .byte  255   ; // unsigned char  ucMDT;  // +16, 1: Manufacturing date(8bit)     .byte  255   ; // unsigned char  ucCRC;  // +17, 1: 7-bit CRC checksum(7bit) 構造体末尾のパディングが考慮されないことを除けば、このメモリ配置はCコンパイラによって構造体アライメントが行われた場合と同じです。 __attribute__((packed))が指定された時、Cコンパイラは明示的なパディング(.space擬似命令)のコード出力を省いて、構造体アライメントを無くしたつもりでいるのですが、 実際にはアセンブラによってフィールド単位のアライメントが行われてしまい、結果、構造体アライメントありの場合と同じメモリ配置になってしまっていた、というわけです。 では、どのようなアセンブラコード出力ならば、正しく構造体アライメント無しのメモリ配置になるのでしょうか? .align擬似命令が、Visual C++における#pragma packみたいなものだと考えると、次のようなアセンブラコードを思いつきます。 [構造体アライメント無しにするには?]     .align 0    ; 次の位置が(1<<0)=1の倍数アドレスから始まるようにする。 cid:     .byte  255     .half  65535     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .word  -1     .byte  255     .byte  255 が、これではダメです。 実際にプログラムを書いてみて、これではダメなことを確認しました。 [構造体アライメント無しにするには?(失敗例)] #include unsigned char vbuff[128 * 88]; //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 //初期化済み構造体の定義を、インラインアセンブラで書き直してみます。 extern MMC_CID_INFO cid; asm("     .align 0    ; 次の位置が(1<<0)=1の倍数アドレスから始まるようにする。 cid:     .byte  255     .half  65535     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .word  -1     .byte  255     .byte  255 "); void pceAppInit() {   pceLCDSetBuffer(vbuff);   pceLCDDispStart(); } void pceAppProc(int count) {   int i;   unsigned char* p;   memset(vbuff, 0, sizeof vbuff);   pceFontSetType(2);   pceFontSetPos(0, 0);   p = (unsigned char*)&cid;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     pceFontPrintf("%02x ", p[i]);   }   pceLCDTrans(); } void pceAppExit() { } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff 00 ff ff ff ff ff ff ff ff ff 00 ff ff ff ff ------------------------------------------------------------------------------------------- C言語で書いた場合の、誤った出力結果と変わっていません。 なぜダメか、マニュアル174ページ(PDFでは190ページ目)の、.align擬似命令の説明を見てください。 .align擬似命令 ●機能  この擬似命令の直後に出現するデータを2^Nバイト境界にアライメントします(N=境界指定値)。 ●注意事項  .align擬似命令は直後にあるデータ定義擬似命令に対してのみ有効です。  したがって、アライメントが必要なデータを定義する場合には、データ定義擬似命令個々に.align擬似命令を使用する必要があります。 そう、.align擬似命令は『直後の』データ定義擬似命令に対してしか、効果がないのです。 直後よりうしろのデータ定義擬似命令は、通常どおり、暗黙のアライメントがなされてしまいます。     .align 0    ; 次の位置が(1<<0)=1の倍数アドレスから始まるようにする。 cid:     .byte  255     .half  65535  ; ここから後ろは、通常どおり、暗黙のアライメントがなされてしまいます。     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .word  -1     .byte  255     .byte  255 直後のデータ擬似命令に対して.alignが有効だとういうのならば、次のように書けばいいのでしょうか? [構造体アライメント無しにするには?]     .align 2    ; 構造体の先頭を4の倍数アドレスから始めるために、先頭の.align 2は必要!! cid:     .byte  255     .align 0    ; 次の.halfの暗黙のアライメントを抑制する。     .half  65535     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .align 0    ; 次の.wordの暗黙のアライメントを抑制する。     .word  -1     .byte  255     .byte  255 それでは、もういちどプログラムを書いて試してみます。 [構造体アライメント無しにするには?(失敗例)] #include unsigned char vbuff[128 * 88]; //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 //初期化済み構造体の定義を、インラインアセンブラで書き直してみます。 extern MMC_CID_INFO cid; asm("     .align 2    ; 構造体の先頭を4の倍数アドレスから始めるために、先頭の.align 2は必要!! cid:     .byte  255     .align 0    ; 次の.halfの暗黙のアライメントを抑制する。     .half  65535     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .align 0    ; 次の.wordの暗黙のアライメントを抑制する。     .word  -1     .byte  255     .byte  255 "); void pceAppInit() {   pceLCDSetBuffer(vbuff);   pceLCDDispStart(); } void pceAppProc(int count) {   int i;   unsigned char* p;   memset(vbuff, 0, sizeof vbuff);   pceFontSetType(2);   pceFontSetPos(0, 0);   p = (unsigned char*)&cid;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     pceFontPrintf("%02x ", p[i]);   }   pceLCDTrans(); } void pceAppExit() { } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff 00 ff ff ff ff ff ff ff ff ff 00 ff ff ff ff ------------------------------------------------------------------------------------------- やっぱりダメです。 何通りか試した結果、どうやら.align擬似命令は「パディングを増やすことはできるが減らすことはできない」ことがわかりました。 たとえば、通常、.byte擬似命令には暗黙のアライメントはありません。 が、構造体の先頭フィールドがunsigned char型であるような場合など、unsigned char値を強制的に4の倍数アドレスに置きたい場合に、 .byte擬似命令の前に.align 2を指定したりするわけで、このような.align擬似命令の使い方はOKです。 が、逆に、もともと暗黙のアライメントによって.half擬似命令や.word擬似命令の直前にパディングが追加されていたものとして、 そのパディングを取り除くために.align 0を指定しても、それは無視されてしまうのです。 以上の振る舞いはマニュアルには載っていないようですけれど、まあ、感覚的に理解できなくもありません。 そんなわけで結局、アセンブラによる暗黙のアライメントを抑制するには、次のように1バイトづつ定義するしかないみたいです。 [構造体アライメント無しにするには?(いちおう成功例ですが…)] #include unsigned char vbuff[128 * 88]; //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 //初期化済み構造体の定義を、インラインアセンブラで書き直してみます。 extern MMC_CID_INFO cid; asm("     .align 2        ; 構造体の先頭を4の倍数アドレスから始めるために、先頭の.align 2は必要!! cid:     .byte  255       ; // + 0, 1: ManufactureID(8bit)     .byte 255,255     ; // + 1, 2: OEM/Application ID(16bit)     .byte  255       ; // + 3, 6: Product name(48bit)     .byte  255       ;     .byte  255       ;     .byte  255       ;     .byte  255       ;     .byte  255       ;     .byte  255       ; // + 9, 1: Product revision(8bit)     .byte  255,255,255,255 ; // +10, 4: Product serial number(32bit)     .byte  255       ; // +14, 1: Manufacturing date(8bit)     .byte  255       ; // +15, 1: 7-bit CRC checksum(7bit) "); void pceAppInit() {   pceLCDSetBuffer(vbuff);   pceLCDDispStart(); } void pceAppProc(int count) {   int i;   unsigned char* p;   memset(vbuff, 0, sizeof vbuff);   pceFontSetType(2);   pceFontSetPos(0, 0);   p = (unsigned char*)&cid;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     pceFontPrintf("%02x ", p[i]);   }   pceLCDTrans(); } void pceAppExit() { } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ------------------------------------------------------------------------------------------- shortやintのフィールドを、人手で1バイトづつ分解して定義するというのは全く現実的ではありませんが、 Cコンパイラがアセンブラコードを自動生成する分には、やってくれても良さそうに思えますが.... たぶん、EPSONコンパイラはこのようなケースを想定していなかったのだと思います。 余談ですが、Linux PC上でGCCを使った場合は、構造体アライメント無しの場合に次のようなアセンブラコードが生成されます。 cid:     .byte  -1     .value -1     .byte  -1     .byte  -1     .byte  -1     .byte  -1     .byte  -1     .byte  -1     .byte  -1     .long  -1     .byte  -1     .byte  -1 最初に見た、P/ECE上での誤った例と同じに見えますが、PCの場合はこれで大丈夫なのです。 Linux PCのアセンブラはP/ECEのアセンブラと違って、初期状態では暗黙のアライメントを行わないみたいです。 ■■■■■■■■■■■■■■■■■■■■ ■構造体パックを使っても大丈夫なケース■ ■■■■■■■■■■■■■■■■■■■■ 以上のように、P/ECE開発環境では__attribute__((packed))による構造体パックがかなり大きな問題を抱えていることがわかりました。 通常、「使わないほうが良い」のは間違いありません。 が、まったく使えないかというとそうでもなくて、よく注意して使えば役に立つケースもあります。 かなり細かい話になりますので、必要なければ読み飛ばしてください。 �―藉�済み構造体を使わないケース ひとつめは、初期化済み構造体を使わないことが確実にわかっているケースです。 P/ECE上で正しく動作しなかったC言語プログラムを、初期化済み構造体を使わないよう、次のように書き換えてみました。 [構造体アライメント無し、初期化済み構造体を使わない] #include unsigned char vbuff[128 * 88]; //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 MMC_CID_INFO cid; void pceAppInit() {   pceLCDSetBuffer(vbuff);   pceLCDDispStart(); } void pceAppProc(int count) {   int i;   unsigned char* p;   //構造体フィールドに値をひとつづつ格納   cid.ucMID = 0xff;   cid.usOID = 0xffff;   cid.ucPNM[0] = 0xff;   cid.ucPNM[1] = 0xff;   cid.ucPNM[2] = 0xff;   cid.ucPNM[3] = 0xff;   cid.ucPNM[4] = 0xff;   cid.ucPNM[5] = 0xff;   cid.ucPRV = 0xff;   cid.ulPSN = 0xffffffff;   cid.ucMDT = 0xff;   cid.ucCRC = 0xff;   memset(vbuff, 0, sizeof vbuff);   pceFontSetType(2);   pceFontSetPos(0, 0);   p = (unsigned char*)&cid;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     pceFontPrintf("%02x ", p[i]);   }   pceLCDTrans(); } void pceAppExit() { } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ------------------------------------------------------------------------------------------- 正しい結果が得られました。 初期化済み構造体の場合はフィールドひとつひとつがデータ定義擬似命令にコンパイルされたため、前述のような暗黙のアライメント問題が発生したわけですが、 未初期化構造体の場合はフィールドひとつひとつに対応する擬似命令が生成されるわけではなく、次のようなアセンブラコードが生成されます。 .comm  cid 16  ; 16バイトのグローバル変数領域を確保せよ、という意味 構造体フィールドに値をひとつづつ格納する部分のC言語プログラムは、次のようにコンパイルされます。(読む必要ないです(^^;) xld.w  %r0,cid xld.w  %r10,0x000000ff xld.b  [%r0],%r10     ; + 0, 1: ManufactureID(8bit) xld.ub  %r11,[%r0+1]    ; + 1, 2: OEM/Application ID(16bit) (手順に無駄がありますが、間違いではないです) or    %r11,%r10 xld.b  [%r0+1],%r11 xld.ub  %r11,[%r0+2] or    %r11,%r10 xld.b  [%r0+2],%r11 xld.b  [%r0+3],%r10    ; + 3, 6: Product name(48bit) xld.b  [%r0+4],%r10 xld.b  [%r0+5],%r10 xld.b  [%r0+6],%r10 xld.b  [%r0+7],%r10 xld.b  [%r0+8],%r10 xld.b  [%r0+9],%r10    ; + 9, 1: Product revision(8bit) xld.ub  %r11,[%r0+10]    ; +10, 4: Product serial number(32bit) (手順に無駄がありますが、間違いではないです) or    %r11,%r10 xld.b  [%r0+10],%r11 xld.ub  %r11,[%r0+11] or    %r11,%r10 xld.b  [%r0+11],%r11 xld.ub  %r11,[%r0+12] or    %r11,%r10 xld.b  [%r0+12],%r11 xld.ub  %r11,[%r0+13] or    %r11,%r10 xld.b  [%r0+13],%r11 xld.b  [%r0+14],%r10    ; +14, 1: Manufacturing date(8bit) xadd   %r1,%r0,15     ; +15, 1: 7-bit CRC checksum(7bit) xld.b  [%r1],%r10 Cコンパイラは、パックされた構造体の各フィールドの正しいオフセットを認識しているので、正しいコードが生成されています。(ちょっと無駄ありますけれど) いま上で示したプログラム例のような、構造体の各フィールドをプログラムでひとつづつ初期化するケースに加えて、 ファイルを読み込んだりデバイスからバイト列を受信して、そのまま構造体変数に格納するケースでも、初期化済み構造体を使う必要がないのでOKです。 ��構造体末尾のパディングだけを除去する場合 もともと構造体アライメントありの場合にもフィールド間にはパディングがなくって、構造体末尾だけにパディングがあるような場合に、 構造体末尾のパディングを取り除いてそのぶん構造体サイズを小さくしたいケースで、__attribute__((packed))が利用可能です。 そんな都合の良い話があるか!と言われそうですが(^^;、あります。 USB仕様の構造体です。 USB仕様の構造体は、各フィールドのオフセットに関してはパディング無しで適切なアライメントになるよう定められていることが多いのですけれど、 なぜか末尾に1バイトのフィールドがあったりして、構造体パック無しでは末尾にパディングが付いてしまうようなケースがよくあるみたいなのです。 たとえば、USB通信端点の特性を記述するための構造体として、エンドポイントディスクリプタという構造体があります。 typedef struct _ENDPOINT_DESCRIPTOR {   unsigned char bLength;          /* + 0, 1 */   unsigned char bDescriptorType;      /* + 1, 1 */   unsigned char bEndpointAddress;      /* + 2, 1 */   unsigned char bAttributes;        /* + 3, 1 */   unsigned short wMaxPacketSize;       /* + 4, 2 */   unsigned char bInterval;         /* + 6, 1 */ } __attribute__((packed)) ENDPOINT_DESCRIPTOR; /* = 7 */ USBデバイス(P/ECE側です)は、デバイスの通信特性をホスト(PC側です)へ知らせるために、エンドポイントディスクリプタをデバイスからホストへ送信します。 エンドポイントディスクリプタの内容が変化することはまずないので、初期化済み構造体としてあらかじめ用意しておきます。 初期化済みエンドポイントディスクリプタをデバイスからホストへ送信するプログラムは、だいたい次のようなかんじになります。 const ENDPOINT_DESCRIPTOR endpoint_desc_ep2in = {   sizeof(ENDPOINT_DESCRIPTOR),  //unsigned char bLength;      /* + 0, 1 */   ENDPOINT_DESCRIPTOR_TYPE,    //unsigned char bDescriptorType;  /* + 1, 1 */   0x82,              //unsigned char bEndpointAddress;  /* + 2, 1 */   2,               //unsigned char bAttributes;    /* + 3, 1 */   EP2_NONISO_PACKET_SIZE,     //unsigned short wMaxPacketSize;   /* + 4, 2 */   0,               //unsigned char bInterval;     /* + 6, 1 */ }; D12_WriteEndpoint(1/*出力端点の指定*/, &endpoint_desc_ep2in, sizeof(ENDPOINT_DESCRIPTOR)); ※本当はエンドポイントディスクリプタだけを単体で送信することはないのですけれど、説明を簡単にするために、ここでは考えないことにします。 以上は、正しく動作するプログラムの例です。 さて、もしもENDPOINT_DESCRIPTORの定義のところで__attribute__((packed))を指定しなかったらどうなるでしょうか? typedef struct _ENDPOINT_DESCRIPTOR {   unsigned char bLength;          /* + 0, 1 */   unsigned char bDescriptorType;      /* + 1, 1 */   unsigned char bEndpointAddress;      /* + 2, 1 */   unsigned char bAttributes;        /* + 3, 1 */   unsigned short wMaxPacketSize;       /* + 4, 2 */   unsigned char bInterval;         /* + 6, 1 */   (パディング)                /* + 7, 1 */ } ENDPOINT_DESCRIPTOR;             /* = 8 */ const ENDPOINT_DESCRIPTOR endpoint_desc_ep2in = {   sizeof(ENDPOINT_DESCRIPTOR),  //unsigned char bLength;      /* + 0, 1 */ ... 本当は7とすべきなのに、8が設定されてしまう!!   ENDPOINT_DESCRIPTOR_TYPE,    //unsigned char bDescriptorType;  /* + 1, 1 */   0x82,              //unsigned char bEndpointAddress;  /* + 2, 1 */   2,               //unsigned char bAttributes;    /* + 3, 1 */   EP2_NONISO_PACKET_SIZE,     //unsigned short wMaxPacketSize;   /* + 4, 2 */   0,               //unsigned char bInterval;     /* + 6, 1 */                   //(パディング)            /* + 7, 1 */ }; D12_WriteEndpoint(1/*出力端点の指定*/, &endpoint_desc_ep2in, sizeof(ENDPOINT_DESCRIPTOR)); // 本当は D12_WriteEndpoint(1, &endpoint_desc_ep2in, 7) とすべきなのに、 // D12_WriteEndpoint(1, &endpoint_desc_ep2in, 8) になってしまう!! bLengthフィールドの値や送信バイト数が正しくないため、このプログラムは正しく動作しません。 bLengthフィールドや送信バイト数として、sizeof(ENDPOINT_DESCRIPTOR)を使うのではなく、明示的に"7"と書けば解決するのですが、 ひとつひとつの構造体サイズを覚えるよりも、できるならばsizeofが使えたほうが楽ですよね。 ちょっと細かい話になりますが、通常、エンドポイントディスクリプタは複数定義します。 が、その際、初期化済みエンドポイントディスクリプタ構造体を、配列にしてはいけません。 エンドポイントディスクリプタ構造体は、単体ならばパディング無しで適切なアライメントになっているのですけれど、 配列にすると二つ目の要素の途中でアライメントが崩れ、アセンブラによる暗黙のアライメントが行われて、不要なパディングが入ってしまうからです。 const ENDPOINT_DESCRIPTOR endpoint_desc[2] = { {   sizeof(ENDPOINT_DESCRIPTOR),  //unsigned char bLength;      /* + 0, 1 */   ENDPOINT_DESCRIPTOR_TYPE,    //unsigned char bDescriptorType;  /* + 1, 1 */   0x82,              //unsigned char bEndpointAddress;  /* + 2, 1 */   2,               //unsigned char bAttributes;    /* + 3, 1 */   EP2_NONISO_PACKET_SIZE,     //unsigned short wMaxPacketSize;   /* + 4, 2 */   0,               //unsigned char bInterval;     /* + 6, 1 */ }, {   sizeof(ENDPOINT_DESCRIPTOR),  //unsigned char bLength;      /* + 7, 1 */   ENDPOINT_DESCRIPTOR_TYPE,    //unsigned char bDescriptorType;  /* + 8, 1 */   0x02,              //unsigned char bEndpointAddress;  /* + 9, 1 */   2,               //unsigned char bAttributes;    /* +10, 1 */   EP2_NONISO_PACKET_SIZE,     //unsigned short wMaxPacketSize;   /* +11, 2 */   0,               //unsigned char bInterval;     /* +13, 1 */ } };                                   /* =14 */ ↓コンパイルすると...   .align 2 endpoint_desc:   .byte  7    //unsigned char bLength;      /* + 0, 1 */   .byte  5    //unsigned char bDescriptorType;  /* + 1, 1 */   .byte 130    //unsigned char bEndpointAddress;  /* + 2, 1 */   .byte  2    //unsigned char bAttributes;    /* + 3, 1 */   .half  64    //unsigned short wMaxPacketSize;   /* + 4, 2 */   .byte  0    //unsigned char bInterval;     /* + 6, 1 */   .byte  7    //unsigned char bLength;      /* + 7, 1 */   .byte  5    //unsigned char bDescriptorType;  /* + 8, 1 */   .byte  2    //unsigned char bEndpointAddress;  /* + 9, 1 */   .byte  2    //unsigned char bAttributes;    /* +10, 1 */            //(ここにパディングが入ってしまう!!) /* +11, 1 */   .half  64    //unsigned short wMaxPacketSize;   /* +12, 2 */ ずれてる!!   .byte  0    //unsigned char bInterval;     /* +14, 1 */ ずれてる!! 複数のエンドポイントディスクリプタを初期化済み構造体として定義する場合は、配列にするのでなく、ひとつひとつ定義し、 実行時にくっつけて送信する必要があります。 const ENDPOINT_DESCRIPTOR endpoint_desc_ep2in = { sizeof(ENDPOINT_DESCRIPTOR), ENDPOINT_DESCRIPTOR_TYPE, 0x82, 2, EP2_NONISO_PACKET_SIZE, 0, }; const ENDPOINT_DESCRIPTOR endpoint_desc_ep2out = { sizeof(ENDPOINT_DESCRIPTOR), ENDPOINT_DESCRIPTOR_TYPE, 0x02, 2, EP2_NONISO_PACKET_SIZE, 0, }; memcpy(&tmpbuf[sizeof(ENDPOINT_DESCRIPTOR) * 0], &endpoint_desc_ep2in, sizeof(ENDPOINT_DESCRIPTOR)); memcpy(&tmpbuf[sizeof(ENDPOINT_DESCRIPTOR) * 1], &endpoint_desc_ep2in, sizeof(ENDPOINT_DESCRIPTOR)); D12_WriteEndpoint(1/*出力端点の指定*/, &tmpbuf, sizeof(ENDPOINT_DESCRIPTOR) * 2); ■■■■■ ■まとめ■ ■■■■■ 結論です。 『P/ECE開発環境では、__attribute__((packed))による構造体パックは利用不可です。』 確実に問題が回避できると確信できる場合に限って、__attribute__((packed))も利用可能ですが、 それはまるで、生のアセンブラやバイナリエディタを使ってデータを操作しているような感覚です。 もともと#pargma packや__attribute__((packed))は処理系依存の拡張機能ですし、使わずに済むならばそれに越したことはありません。 * Sat Nov 6 00:00:00 JST 2004 Naoyuki Sawa - 構造体パックに問題あり#1 P/ECE開発環境のCコンパイラには問題があり、構造体パックを行う場合に注意が必要です。 今回は、この問題について記しておこうと思います。 P/ECE特有の問題点だけ読みたいかたは、■P/ECEの場合〜問題発生!!■まで読み飛ばしてください。 ■■■■■■■■■■■■■■■■■ ■一般的な話〜構造体アライメント■ ■■■■■■■■■■■■■■■■■ 通常、C言語で構造体を定義すると、各フィールドがフィールドサイズの倍数の位置に揃うように、暗黙の空白領域が追加されます。 たとえば次のような構造体を定義したとします。 typedef struct _SAMPLE1 {   char c1;   short s2;   int i3;   char c4; } SAMPLE1; これだけ見ると、各フィールドのオフセットとサイズ、および構造体サイズは次のようになるように見えるのですけれど、 (コメント表記は /* +フィールドオフセット, フィールドサイズ */、/* =構造体サイズ */ です。) [誤った配置] typedef struct _STRUCT1 {   char c1;  /* + 0, 1 */   short s2;  /* + 1, 2 */   short s3;  /* + 3, 2 */   int i4;  /* + 5, 4 */   char c5;  /* + 9, 1 */ } STRUCT1;    /* =10 */ 実際には、各フィールドのオフセットとサイズ、および構造体サイズは次のようになります。 [本当の配置] typedef struct _STRUCT1 {   char c1;  /* + 0, 1 */   (パディング) /* + 1, 1 */   short s2;  /* + 2, 2 */   short s3;  /* + 4, 2 */   (パディング) /* + 6, 2 */   int i4;  /* + 8, 4 */   char c5;  /* +12, 1 */   (パディング) /* +13, 3 */ } STRUCT1;    /* =16 */ s2フィールド(2バイト整数)に注目してください。 [誤った配置]では、s2フィールドがオフセット1(2の倍数でない)から始まっています。 2の倍数でないアドレスに置かれた2バイト整数の読み書きは、CPUの種類によってはできなかったり、できても効率が悪かったりします。 そこで、2バイト整数が2の倍数のアドレスから始まるよう、s2フィールドの直前に暗黙の空白領域が追加されるのです。 暗黙の空白領域のことを、“パディング”(詰め物)と呼んだりします。 i4フィールド(4バイト整数)についても同様です。 4の倍数でないアドレスに置かれた4バイト整数の読み書きは、CPUの種類によってはできなかったり、できても効率が悪かったりします。 そこで、4バイト整数が4の倍数のアドレスから始まるよう、i4フィールドの直前にパディングが追加されます。 一見、構造体末尾のパディングは不要に見えます。 なぜこれが必要かというと、構造体配列を定義したときのためです。 もしも構造体末尾の空白領域がなかったとすると、次のような構造体配列を定義したときに、 STRUCT1 struct1[2]; メモリ上の配置は次のようになってしまいます。 [誤った配置] // struct1[0] char c1;  /* + 0, 1 */ (パディング) /* + 1, 1 */ short s2;  /* + 2, 2 */ short s3;  /* + 4, 2 */ (パディング) /* + 6, 2 */ int i4;  /* + 8, 4 */ char c5;  /* +12, 1 */ // struct1[1] char c1;  /* +13, 1 */ (パディング) /* +14, 1 */ short s2;  /* +15, 2 */ short s3;  /* +17, 2 */ (パディング) /* +19, 2 */ int i4;  /* +21, 4 */ char c5;  /* +25, 1 */ struct1[0]は適切なフィールド配置ですが、struct1[1]のs2やs3、i4の配置が適切でありません。 s2とs3は2バイト整数なのに2の倍数アドレスに配置されず、i4は4バイト整数なのに4の倍数アドレスに配置されていません。 このような問題を避けるため、構造体サイズが「構造体の中のいちばん大きなフィールドのサイズの倍数」になるよう、構造体の末尾にもパディングが追加されます。 [正しい配置] // struct1[0] char c1;  /* + 0, 1 */ (パディング) /* + 1, 1 */ short s2;  /* + 2, 2 */ short s3;  /* + 4, 2 */ (パディング) /* + 6, 2 */ int i4;  /* + 8, 4 */ char c5;  /* +12, 1 */ (パディング) /* +13, 3 */ // struct1[1] char c1;  /* +16, 1 */ (パディング) /* +17, 1 */ short s2;  /* +18, 2 */ short s3;  /* +20, 2 */ (パディング) /* +22, 2 */ int i4;  /* +24, 4 */ char c5;  /* +28, 1 */ (パディング) /* +29, 3 */ 以上のように、コンパイラが自動的にパディングを追加して、各フィールドの配置を適切に保つことを、“構造体アライメント”と呼んだりします。 ■■■■■■■■■■■■■■■■■■■■■■ ■一般的な話〜構造体アライメントを抑制する■ ■■■■■■■■■■■■■■■■■■■■■■ 通常は、コンパイラが自動的に構造体アライメントを行ってくれるので、プログラマは特に気にする必要はありません。 が、稀に、構造体アライメントが行われては困る場合があります。 ネットワークやデバイスとの通信など、あらかじめ仕様で決められたフィールド配置の構造体を送受信する場合です。 ネットワークやデバイスとの通信では、通信データ量を少なくするために、2バイト整数や4バイト整数もパディング無しでびっちり詰め込まれている場合が多いです。 メモリ上に構造体を作っておいて、そのメモリイメージを直接送信する場合や、メモリ上の構造体変数に直接受信する場合、 構造体のフィールド配置はあらかじめ決められた仕様にきっちり合っていなければいけません。 たとえば、MMC(Multi Media Card)との通信では、次のような構造体の仕様が定められています。 (まどかの世界さんの、MMC対応カーネルのソースから引用させていただきました) [正しい配置] //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char ucMID;   // + 0, 1: ManufactureID(8bit)   unsigned short usOID;   // + 1, 2: OEM/Application ID(16bit)   unsigned char ucPNM[6];  // + 3, 6: Product name(48bit)   unsigned char ucPRV;   // + 9, 1: Product revision(8bit)   unsigned long ulPSN;   // +10, 4: Product serial number(32bit)   unsigned char ucMDT;   // +14, 1: Manufacturing date(8bit)   unsigned char ucCRC;   // +15, 1: 7-bit CRC checksum(7bit) } MMC_CID_INFO;         // =16 ところが、コンパイラによって構造体アライメントが行われると、実際の配置は次のようになってしまいます。 [誤った配置] //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char ucMID;   // + 0, 1: ManufactureID(8bit)   (パディング) // + 1, 1:   unsigned short usOID;   // + 2, 2: OEM/Application ID(16bit)   unsigned char ucPNM[6];  // + 4, 6: Product name(48bit)   unsigned char ucPRV;   // +10, 1: Product revision(8bit)   (パディング) // +11, 1:   unsigned long ulPSN;   // +12, 4: Product serial number(32bit)   unsigned char ucMDT;   // +16, 1: Manufacturing date(8bit)   unsigned char ucCRC;   // +17, 1: 7-bit CRC checksum(7bit)   (パディング) // +18, 2: } MMC_CID_INFO;         // =20 これではダメです。 そこで、コンパイラによる構造体アライメントを抑制し、パディングをなくす方法が用意されています。 構造体アライメントを抑制する方法は、コンパイラ毎に違っています。 たとえば、Visual C++の場合は、次のようにします。 #pragma pack(1) //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char ucMID;   // + 0, 1: ManufactureID(8bit)   unsigned short usOID;   // + 1, 2: OEM/Application ID(16bit)   unsigned char ucPNM[6];  // + 3, 6: Product name(48bit)   unsigned char ucPRV;   // + 9, 1: Product revision(8bit)   unsigned long ulPSN;   // +10, 4: Product serial number(32bit)   unsigned char ucMDT;   // +14, 1: Manufacturing date(8bit)   unsigned char ucCRC;   // +15, 1: 7-bit CRC checksum(7bit) } MMC_CID_INFO;         // =16 #pragma pack() GCCの場合は、次のようにします。 //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 ※厳密に言うと、MMCとの通信データはバイト順がモトローラ並びで、PentiumやS1C33(P/ECEのCPU)のインテル並びとは逆です。  だから、構造体アライメントだけ無くしても、メモリイメージそのままで送受信することはできません。  が、説明を簡単にするために、ここではバイト順の違いは考えないことにしてください。 それでは、本当に構造体アライメントが抑制されているか、確認してみましょう。 テストのために、メモリ上に初期化済みの構造体を用意し、その内容を1バイトづつ表示してみることにします。 まずは、構造体アライメントを★抑制しない★場合のメモリ内容を確かめてみます。 [Windows PC & Visual C++ 6.0、構造体アライメントあり] #include //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;   // + 0, 1: ManufactureID(8bit)   unsigned short usOID;   // + 2, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];  // + 4, 6: Product name(48bit)   unsigned char  ucPRV;   // +10, 1: Product revision(8bit)   unsigned long  ulPSN;   // +12, 4: Product serial number(32bit)   unsigned char  ucMDT;   // +16, 1: Manufacturing date(8bit)   unsigned char  ucCRC;   // +17, 1: 7-bit CRC checksum(7bit) } MMC_CID_INFO;         // =20 MMC_CID_INFO cid = {   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)   0xffff,                 // unsigned short usOID;  // + 2, 2: OEM/Application ID(16bit)   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 4, 6: Product name(48bit)   0xff,                  // unsigned char  ucPRV;  // +10, 1: Product revision(8bit)   0xffffffff,               // unsigned long  ulPSN;  // +12, 4: Product serial number(32bit)   0xff,                  // unsigned char  ucMDT;  // +16, 1: Manufacturing date(8bit)   0xff,                  // unsigned char  ucCRC;  // +17, 1: 7-bit CRC checksum(7bit) }; int main() {   int i;   unsigned char* p;   p = (unsigned char*)&cid;   printf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     printf("%02x ", p[i]);   }   return 0; } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 20 ff 00 ff ff ff ff ff ff ff ff ff 00 ff ff ff ff ff ff 00 00 ------------------------------------------------------------------------------------------- 構造体アライメントありなので、パディングが追加されています。(00の部分) 構造体サイズもパディングの分増えて、MMC仕様よりも4バイト多い20バイトになっています。 次に、構造体アライメントを★抑制した★場合のメモリ内容を確かめてみます。 [Windows PC & Visual C++ 6.0、構造体アライメント無し] #include #pragma pack(1) //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;   // + 0, 1: ManufactureID(8bit)   unsigned short usOID;   // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];  // + 3, 6: Product name(48bit)   unsigned char  ucPRV;   // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;   // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;   // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;   // +15, 1: 7-bit CRC checksum(7bit) } MMC_CID_INFO;         // =16 #pragma pack() MMC_CID_INFO cid = {   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)   0xffff,                 // unsigned short usOID;  // + 1, 2: OEM/Application ID(16bit)   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 3, 6: Product name(48bit)   0xff,                  // unsigned char  ucPRV;  // + 9, 1: Product revision(8bit)   0xffffffff,               // unsigned long  ulPSN;  // +10, 4: Product serial number(32bit)   0xff,                  // unsigned char  ucMDT;  // +14, 1: Manufacturing date(8bit)   0xff,                  // unsigned char  ucCRC;  // +15, 1: 7-bit CRC checksum(7bit) }; int main() {   int i;   unsigned char* p;   p = (unsigned char*)&cid;   printf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     printf("%02x ", p[i]);   }   return 0; } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ------------------------------------------------------------------------------------------- パディングが消えて、構造体サイズもMMC仕様どおりの16バイトになりました。 GCCの場合も同様です。 まずは、構造体アライメントを★抑制しない★場合。 [Linux PC & GCC 3.3.3、構造体アライメントあり] #include //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;   // + 0, 1: ManufactureID(8bit)   unsigned short usOID;   // + 2, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];  // + 4, 6: Product name(48bit)   unsigned char  ucPRV;   // +10, 1: Product revision(8bit)   unsigned long  ulPSN;   // +12, 4: Product serial number(32bit)   unsigned char  ucMDT;   // +16, 1: Manufacturing date(8bit)   unsigned char  ucCRC;   // +17, 1: 7-bit CRC checksum(7bit) } MMC_CID_INFO;         // =20 MMC_CID_INFO cid = {   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)   0xffff,                 // unsigned short usOID;  // + 2, 2: OEM/Application ID(16bit)   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 4, 6: Product name(48bit)   0xff,                  // unsigned char  ucPRV;  // +10, 1: Product revision(8bit)   0xffffffff,               // unsigned long  ulPSN;  // +12, 4: Product serial number(32bit)   0xff,                  // unsigned char  ucMDT;  // +16, 1: Manufacturing date(8bit)   0xff,                  // unsigned char  ucCRC;  // +17, 1: 7-bit CRC checksum(7bit) }; int main() {   int i;   unsigned char* p;   p = (unsigned char*)&cid;   printf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     printf("%02x ", p[i]);   }   return 0; } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 20 ff 00 ff ff ff ff ff ff ff ff ff 00 ff ff ff ff ff ff 00 00 ------------------------------------------------------------------------------------------- 次に、構造体アライメントを★抑制した★場合。 [Linux PC & GCC 3.3.3、構造体アライメント無し] #include //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 MMC_CID_INFO cid = {   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)   0xffff,                 // unsigned short usOID;  // + 1, 2: OEM/Application ID(16bit)   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 3, 6: Product name(48bit)   0xff,                  // unsigned char  ucPRV;  // + 9, 1: Product revision(8bit)   0xffffffff,               // unsigned long  ulPSN;  // +10, 4: Product serial number(32bit)   0xff,                  // unsigned char  ucMDT;  // +14, 1: Manufacturing date(8bit)   0xff,                  // unsigned char  ucCRC;  // +15, 1: 7-bit CRC checksum(7bit) }; int main() {   int i;   unsigned char* p;   p = (unsigned char*)&cid;   printf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     printf("%02x ", p[i]);   }   return 0; } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ------------------------------------------------------------------------------------------- 期待通りです。 構造体アライメントを抑制することを、“構造体パック”と呼んだりします。 ■■■■■■■■■■■■■■■■ ■P/ECEの場合〜問題発生!!■ ■■■■■■■■■■■■■■■■ 上述のGCCの例は、PC上でLinux(FedoraCore2)を使って行いました。 さて、P/ECE開発環境のCコンパイラもGCCです。 ちょっとバージョンが古いけど、PC上でLinuxを使った場合と同じ結果が期待されます。 まずは、構造体アライメントを★抑制しない★場合。 [P/ECE & pcc33.exe、構造体アライメントあり] #include unsigned char vbuff[128 * 88]; //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;   // + 0, 1: ManufactureID(8bit)   unsigned short usOID;   // + 2, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];  // + 4, 6: Product name(48bit)   unsigned char  ucPRV;   // +10, 1: Product revision(8bit)   unsigned long  ulPSN;   // +12, 4: Product serial number(32bit)   unsigned char  ucMDT;   // +16, 1: Manufacturing date(8bit)   unsigned char  ucCRC;   // +17, 1: 7-bit CRC checksum(7bit) } MMC_CID_INFO;         // =20 MMC_CID_INFO cid = {   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)   0xffff,                 // unsigned short usOID;  // + 2, 2: OEM/Application ID(16bit)   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 4, 6: Product name(48bit)   0xff,                  // unsigned char  ucPRV;  // +10, 1: Product revision(8bit)   0xffffffff,               // unsigned long  ulPSN;  // +12, 4: Product serial number(32bit)   0xff,                  // unsigned char  ucMDT;  // +16, 1: Manufacturing date(8bit)   0xff,                  // unsigned char  ucCRC;  // +17, 1: 7-bit CRC checksum(7bit) }; void pceAppInit() {   pceLCDSetBuffer(vbuff);   pceLCDDispStart(); } void pceAppProc(int count) {   int i;   unsigned char* p;   memset(vbuff, 0, sizeof vbuff);   pceFontSetType(2);   pceFontSetPos(0, 0);   p = (unsigned char*)&cid;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     pceFontPrintf("%02x ", p[i]);   }   pceLCDTrans(); } void pceAppExit() { } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 20 ff 00 ff ff ff ff ff ff ff ff ff 00 ff ff ff ff ff ff 00 00 ------------------------------------------------------------------------------------------- あとは、構造体アライメントを★抑制した★場合が期待通りの結果ならば、全てOKなのですが・・・ [P/ECE & pcc33.exe、構造体アライメント無し] #include unsigned char vbuff[128 * 88]; //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 MMC_CID_INFO cid = {   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)   0xffff,                 // unsigned short usOID;  // + 1, 2: OEM/Application ID(16bit)   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 3, 6: Product name(48bit)   0xff,                  // unsigned char  ucPRV;  // + 9, 1: Product revision(8bit)   0xffffffff,               // unsigned long  ulPSN;  // +10, 4: Product serial number(32bit)   0xff,                  // unsigned char  ucMDT;  // +14, 1: Manufacturing date(8bit)   0xff,                  // unsigned char  ucCRC;  // +15, 1: 7-bit CRC checksum(7bit) }; void pceAppInit() {   pceLCDSetBuffer(vbuff);   pceLCDDispStart(); } void pceAppProc(int count) {   int i;   unsigned char* p;   memset(vbuff, 0, sizeof vbuff);   pceFontSetType(2);   pceFontSetPos(0, 0);   p = (unsigned char*)&cid;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     pceFontPrintf("%02x ", p[i]);   }   pceLCDTrans(); } void pceAppExit() { } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff 00 ff ff ff ff ff ff ff ff ff 00 ff ff ff ff ------------------------------------------------------------------------------------------- 問題発生です!! 構造体サイズは確かに仕様どおりの16バイトになっているのですが、なぜかパディングが残っています。 わかりやすいように、結果出力を並び替えてみます。 ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff         // + 0, 1: ManufactureID(8bit) 00         // + 1, 1: なぜかパディングが残っている? ff ff        // + 2, 2: OEM/Application ID(16bit) ff ff ff ff ff ff  // + 4, 6: Product name(48bit) ff         // +10, 1: Product revision(8bit) 00         // +11, 1: なぜかパディングが残っている? ff ff ff ff     // +12, 4: Product serial number(32bit) ??         // +??, 1: Manufacturing date(8bit) はどこへ行った? ??         // +??, 1: 7-bit CRC checksum(7bit) はどこへ行った? ------------------------------------------------------------------------------------------- いったいどうなっているのでしょう? この問題、P/ECE開発環境のCコンパイラ(gcc33.exe)とアセンブラ(as33.exe)の協調が上手くいっていないことが原因なのです。 詳しくは、次回に。 (続きます…) * Sat May 16 00:00:00 JST 2004 Naoyuki Sawa - Zlibを実装してみる#2 前回は、省メモリなZlib展開ルーチンを実装してみました。 また、そのルーチンを使ってZlib形式の圧縮ファイルを展開し、画面表示するサンプルを作成しました。 しかしながら、Zlib形式の圧縮ファイルは、そのままでは少々使いづらい面があります。 いちばんの問題は、Zlib形式に対応した圧縮・展開ツールが普及していない、という点です。 Zlibの仕様書によると、実は「Zlib形式」とは、圧縮方法まで含めた仕様ではないそうです。 圧縮方法を示したヘッダと、圧縮データ本体、そして検査用コードの並びを既定しているだけの仕様です。 図で示すと、こんな感じです。 +===========================+ |Zlibヘッダ                    |←Zlib仕様 +−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |Zlibヘッダに書いてある圧縮方法を使った、圧縮データ| +−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |検査用コード                     |←Zlib仕様 +===========================+ Windows WAVファイル形式が、PCM形式・ADPCM形式・MP3形式など、いろいろな内部形式を持っているのに似ています。 さて、現在のところZlib形式で利用可能な圧縮方法としては、「Deflate圧縮形式」だけが規定されています。 一般にZlib形式と呼ばれるのは、厳密には「Deflate圧縮形式の圧縮データを持った、Zlib形式のファイル」のことだったのです。 もちろん、前回実装したZlib展開ルーチンも、Deflate圧縮形式に基くものです。 先ほどの図を書き直すと、次のようになります。 +===============+ |Zlibヘッダ        |←Zlib仕様 +−−−−−−−−−−−−−−−+ |Deflate形式の圧縮データ| +−−−−−−−−−−−−−−−+ |検査用コード         |←Zlib仕様 +===============+ Zlib形式の圧縮ファイルを扱うには、圧縮・展開ツールがZlib形式のヘッダや検査用コードを認識できなければいけません。 複数の形式に対応した圧縮・展開ツールは様々ありますが、Zlib形式を扱うことのできるツールは見つけられませんでした。 僕は、こちらのサンプルプログラムをそのままZlib圧縮・展開ツールとして使わせて頂いています。 「Haruhiko Okumura's Home Page(http://www.matsusaka-u.ac.jp/~okumura/)」さんのサイトの、 「zlib 入門(http://www.matsusaka-u.ac.jp/~okumura/compression/zlib.html)」のページの、 「comptest.c(http://www.matsusaka-u.ac.jp/~okumura/compression/comptest.c)」。 コンパイルするためには、オリジナルのZlib(http://www.gzip.org/zlib/)が必要です。 さて、Zlib形式よりも圧倒的にメジャーな圧縮ファイル形式として、「Zip形式」があります。 Zip形式も、Zlib形式と同じく、圧縮方法までを定めた仕様ではありません。 様々な圧縮方法を使った圧縮データの格納方法や、ファイル情報を付加する方法等を定めた仕様です。 Zip形式が扱う“様々な圧縮方法”の中でも特に良く利用されるのが、前出の「Deflate圧縮形式」です。 同じDeflate圧縮形式を使ったZlib形式とZip形式は、ガワが違うだけで本質はほとんど同じ、と言えます。 Zlib形式と違って、Zip形式は複数のファイルをまとめて格納できるので、図で示すとこんな感じになります。 +================================+ |Zipファイルヘッダ1、検査用コードも含む           |←Zip仕様 +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |Zipファイルヘッダ1に書いてある圧縮方法を使った、圧縮データ1| +================================+ |Zipファイルヘッダ2、検査用コードも含む           |←Zip仕様 +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |Zipファイルヘッダ2に書いてある圧縮方法を使った、圧縮データ2| +================================+ |               ・                | |               ・                | |               ・                | +================================+ |ZipファイルヘッダN、検査用コードも含む           |←Zip仕様 +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |ZipファイルヘッダNに書いてある圧縮方法を使った、圧縮データN| +================================+ |その他のZipファイル情報                   |←Zip仕様 +================================+ あるZipファイル内の圧縮データが、全てDeflate圧縮形式を利用していたならば、先ほどの図は次のようになります。 +=====================+ |Zipファイルヘッダ1、検査用コードも含む|←Zip仕様 +−−−−−−−−−−−−−−−−−−−−−+ |Deflate形式の圧縮データ1     | +=====================+ |Zipファイルヘッダ2、検査用コードも含む|←Zip仕様 +−−−−−−−−−−−−−−−−−−−−−+ |Deflate形式の圧縮データ2     | +=====================+ |          ・          | |          ・          | +=====================+ |ZipファイルヘッダN、検査用コードも含む|←Zip仕様 +−−−−−−−−−−−−−−−−−−−−−+ |Deflate形式の圧縮データN     | +=====================+ |その他のZipファイル情報        |←Zip仕様 +=====================+ Zipファイルに格納する圧縮データの形式としては、Deflate圧縮形式以外にもいくつかの圧縮方法が規定されています。 しかし、手元にある多くのZipファイルを調べてみたところ、実際に使われていた圧縮方法は二種類だけでした。 ・無圧縮 ・Deflate圧縮形式 従って、これら二種類の圧縮形式に対応するだけで、実用上は充分なZip展開ルーチンとなるはずです。 無圧縮形式については、格納されているデータをそのままコピーするだけです。 Deflate圧縮形式については、前回作成したZlib展開ルーチンの内部処理が流用できそうです。 Zlibヘッダと検査コードを扱っている部分のプログラムを少し修正するだけで、Zip展開ルーチンを作成できました。 Zip展開ルーチンを利用したサンプルはこちら。 P/ECE用とWindows用のテストプログラムを用意しました。 P/ECE用テストプログラムは、Zipファイルに格納された画像ファイルを、次々自動的に表示します。 Windowsテストプログラム用は、Zipファイルに格納されたファイル一覧から、選択された画像を展開して表示します。 http://www.piece-me.org/archive/pzltest-20040316-piece.zip http://www.piece-me.org/archive/pzltest-20040316-win32.zip 前回と今回とで、Zlib/Zip展開ルーチンを実装してみました。 二年近く、よくわからないままに使っていたZlibライブラリでしたが、今回のプログラミングでやっと理解することができました。 Zlib/Zip形式の仕組みを理解するために、以下の記事がとても参考になりました。 お礼を申し上げるとともに、ご紹介させて頂きます。 「LabVIEW情報交換のページ(http://www.asahi-net.or.jp/~WR9K-OOHS/)」さんのサイトの、 「ナイジェル山口コーナー(http://www.asahi-net.or.jp/~WR9K-OOHS/Pages/Nigel's/nigel.html)」のページ。 Zlibの仕様書の、いちばん重要な部分に関する和訳があります。 また、圧縮技法の詳細について、わかりやすく論じられています。 圧縮技法以外にも、興味深い記事がたくさんあります。 「KONE's D−Station(http://kone.vis.ne.jp/)」さんのサイトの、 「気まぐれな戯れ言(http://kone.vis.ne.jp/diary/)」のページ。 CRCの計算方法が、順を追って解説されています。 Zip形式で使われているCRC-32の計算方法、多くの実装例はテーブルを利用していますが、そのテーブルはどこから出てくるのか? こちらのページを読んで疑問が解決しました。 今回の作成したZip展開ルーチンは、テーブルを利用しない基本的な方法で実装してみました。 * Sat May 13 16:00:00 JST 2004 Naoyuki Sawa - Zlibを実装してみる#1 2002年6月14日〜17日のP/ECE研究記録「zlibを使う#1〜3」にて、P/ECE用プログラムでZlibを使う方法を調べました。 その後、いくつかの自作プログラムで、実際にZlibを利用しました。 それらは一応上手く動作したのですが、P/ECE用プログラムではZlibが使いづらいと感じる点もいくつかありました。 いちばんの問題は、「zlibを使う#1〜3」でも書いたように、メモリ消費量が多すぎる点です。 圧縮データを格納しておく入力バッファ、展開データを作成するための出力バッファの他に、約35キロバイトの作業用メモリが必要です。 P/ECE用プログラムでは、実際に利用する圧縮データや展開データはさほど大きくありません。たいてい数キロバイト程度です。 それよりも、Zlib内部の一時的な作業用メモリのために、35キロバイトも必要なのがかなり厳しいところです。 Zlibが作業用メモリを確保できないと、展開処理が失敗してしまいます。 その後「P/ECE HAND BOOK」に、P/ECEカーネルソースの展開ルーチンを使う方法が掲載されました。 P/ECEカーネルソースの展開ルーチンも同じZlibですが、P/ECEのメモリ構成に特化していて、より少ないメモリで展開処理が可能です。 また、「P/ECE HAND BOOK」の方法を元にして、複数の圧縮ファイルをまとめて扱う方法が、 「てとら★ぽっと(http://www.zurachu.net/)」さんのサイトで紹介(http://www.zurachu.net/piece/tips/ppack.html)されています。 Zlibにこだわらなければ、より高速な圧縮・展開方法が「p/ware(http://park17.wakwak.com/~hitode/piece/index.html)」さんのサイトで 紹介(http://park17.wakwak.com/~hitode/piece/index.html#plz)されています。 これらの方法を使わせてもらえば実用上は問題解決なのですが、もう少しだけ素のZlibにこだわってみたいと思います。 そもそも僕がZlibの内部動作を理解しないまま使っていたのが問題なので、この機会に勉強してみようというわけです。 さて、なぜZlibはこんなに大きな作業用メモリを必要とするのでしょうか? Zlibの圧縮・展開アルゴリズムは、「ハフマン符号化」と「スライド辞書圧縮」に基づいています。 ハフマン符号化は、頻繁に現れる値に小さな符号、稀に現れる値に大きな符号を割り振って、全体のサイズを小さくする方法です。 ハフマン符号化の展開処理には、「ハフマン木」というデータ構造が利用されます。 「ハフマン木」は、ハフマン符号から元の値に変換するための辞書となります。 「ハフマン木」を作成するために、いくらかのメモリが必要です。 これが、Zlibが大きな作業用メモリを必要とする理由の一つ目です。 しかし実際には、Zlibで利用される「ハフマン木」はさほど大きなデータではありません。せいぜい数キロバイト程度です。 より大きなメモリを必要としているのは、スライド辞書圧縮アルゴリズムで利用される「スライド窓」データ構造です。 スライド辞書圧縮とは、同じパターンのデータ並びを距離と長さに置き換えて、全体のサイズを小さくする方法です。 例えば「012345678901234」というデータ並びがあった場合、次のような感じで圧縮します。 「0123456789、10文字前から、5文字繰り返し」 ところで、Zlibの実装は入力も出力も基本的にストリーミングです。 つまり、一度読み込んだデータは再び読み込めない、一度出力したデータはもう忘れた、というわけです。 しかし、10文字前のデータをもう一度出力するためには、10文字前に出力したデータを覚えておかなければいけません。 そこで、いくらかの出力バッファを用意し、過去何文字分かの出力履歴を記憶しておきます。 これを「スライド窓」と呼びます。 Zlibの仕様では、最大で「3万2千文字前から、258文字繰り返し」という指示が有り得ることになっています。 従って、過去3万2千文字分の出力履歴を記憶しておかなければいけません。 つまり、32キロバイトのスライド窓が必要となります。 これで疑問が解決しました。35キロバイトの作業用メモリの大部分は、スライド窓に使われていたのです。 さて、さきほど“Zlibの実装は入力も出力も基本的にストリーミング”だからスライド窓が必要、と書きました。 より限定すると、“出力がストリーミングだから”出力履歴を記憶しておくためにスライド窓が必要となります。 では、出力をストリーミングにする必要はあるのでしょうか? 僕が作ったP/ECE&Zlib使用のプログラムで、出力ストリーミングを活用したケースはたった一度だけでした。 「VGMプレイヤー」です。 VGMプレイヤーでは、VGZ形式で圧縮された演奏データを少しづつ展開しながら演奏処理を行いました。 しかしこれとて全展開データは充分P/ECEのメモリに収まる程度のサイズで、むしろ作業用メモリの方が大きいぐらいです。 つまり、P/ECE上でZlibの出力ストリーミングが必要となるケースはなかった、と言えます。 より大きな圧縮・展開データを扱う、PC上のプログラムではいくぶん事情も変わってくるのでしょうが、 少なくともゲームプログラムにおいては全データをオンメモリに展開するケースが大部分なのではないかと思います。 そんなわけで、どうやらZlibの出力ストリーミングは僕には必要ないようです。 Zlibの出力ストリーミングをとりやめて、作業用メモリの必要量を減らすことにしました。 さっそくZlibのソース改造にとりかかったのですが・・・難解です(^^; Zlib展開の手順は、さほど複雑なものではありません。 本家「The gzip home page(http://www.gzip.org/)」さんのサイトの、Zlib Home Site(http://www.gzip.org/zlib/)」 →「zlib Documentation(http://www.gzip.org/zlib/zlib_docs.html)」のページから、Zlibの仕様書が入手できます。 ハフマン木作成のところが多少トリッキーですけれど、それ以外は解り易くてシンプルな仕様です。 にも関わらずZlibのソースが難解なのは、移植性と高速化のため、プログラムが複雑化しているのが原因のようです。 特にハフマン復号処理の高速化に関しては相当に力が入っているようで、ソース全体のかなりの部分を占めています。 これらの複雑なルーチンに影響を与えないよう、出力ストリーミングだけを取りやめるのは、ちょっと危険な感じです。 そこで今回は、Zlibのソースを改造するのではなく、自前で再実装してみることにしました。 できあがったのがこちらです。 http://www.piece-me.org/archive/pzltest-20040313-piece.zip http://www.piece-me.org/archive/pzltest-20040313-win32.zip (ファイル内容は、README.TXTをご覧下さい。詳細は次回に。) ※2004/03/14追記: バグ修正および再調整しました。  この記事のいちばん最後に、更新版のURLがあります。 P/ECE用とWindows用のテストプログラムを用意してみました。 いずれもZlib圧縮された画像ファイルを展開し、画面表示するテストプログラムです。 テストプログラムはP/ECE用とWindows用で別々ですが、Zlib展開ルーチンのソースはほぼ共通となっています。 出力ストリーミングを取りやめたので、スライド窓が不要となり、作業用メモリは8キロバイト程度に減りました。 もっと減らしても大丈夫そうなのですが、それは今後の調整課題です。 オリジナルのZlibと異なり、移植性・高速化に関する部分はバッサリです。 移植性に関しては、僕は当面P/ECE、Windows、あとPC用のLinuxぐらいで動けば充分なので、ほとんど何も考えていません。 32ビット環境で、バイトオーダーもリトルエンディアンに決め打ちです。 高速化も全く行っていませんが、P/ECEで扱う程度のデータサイズならば、あまり違いは感じられないはずです。 大まかに言って、1秒当り10〜15キロバイトぐらいの展開速度だと思います。 徹底的に簡素化したので、プログラムサイズはヘッダとソース合わせて1000行程度に小さくなりました。 オリジナルのZlibも、もともとはこれぐらい簡素だったと思うのです。 長く使われていろいろな環境に移植され、また強力に高速化された結果、現在のように複雑な実装になったのでしょう。 今回実装したZlibルーチンは、言わば先祖還りのようなものです。 P/ECEぐらいの規模の環境には、これぐらい簡素な実装で充分なのではないかなぁ、と思いました。 ・・・と、実装してみてからP/ECEカーネルソースの展開ルーチンを読み返すと、図らずも似た感じになってますね。 考えることは同じですねぇ(^^; (続きます…) ---------------------------------------------------------------------------------------------------- 2004/03/14追記: ・バグ修正しました。  大きな圧縮データを展開すると、展開処理に失敗してしまっていたバグを修正しました。 ・作業用メモリサイズを調整しました。  昨日のバージョンでは、ハフマン木作成のために8キロバイトの作業用メモリを確保していました。  いくつかの圧縮データを試してみたところ、実際に使われているのはその1/6程度でした。  ちょっと余裕がありすぎるので、作業用メモリを半分の4キロバイトに減らしてみました。  まだ3倍程度の余裕がありますから、たぶん大丈夫だとは思いますが、ちょっと注意が必要です。  オリジナルのZlibでも、ハフマン木作成のための作業用メモリサイズは、実験で決めているようです。  作業用メモリの最大必要サイズは、理論で求められないみたいです。 ダウンロードはこちら: http://www.piece-me.org/archive/pzltest-20040314-piece.zip http://www.piece-me.org/archive/pzltest-20040314-win32.zip ----------------------------------------------------------------------------------------------------