ファイルローカル変数の再配置情報

static int s1 = 0x7fffffff;
static int s2 = 0x12345678;
int main(int argc, char **argv)
{
    s1 = 0x7f7f7f7f;
    s2 = 0x87654321;

    return 0;
}

.symtabは、シンボルに関する属性情報(そのシンボルがメモリ上に実体を持つセクションのセクション番号と、当該セクション先頭からのオフセット、オブジェクトサイズ、シンボルタイプ(変数か関数か))を一元的に管理するセクションである。シンボルが初期値を持つファイルローカルな変数の場合、そのバイト実体は、.dataセクションに作成される。

$ readelf -s main.o
Symbol table '.symtab' contains 17 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     5: 00000000     4 OBJECT  LOCAL  DEFAULT    3 s1
     6: 00000004     4 OBJECT  LOCAL  DEFAULT    3 s2

上記シンボルテーブル(抜粋)から、変数s1,s2について、以下のことが分かる。

  1. メモリ上の実体が作成されるセクションのセクション番号は、.symtabのNdxで示される。s1,s2とも、3番セクションに実体を持つ。
  2. メモリ上の実体が作成されるセクション先頭からのオフセットは、.symtabのValueで示される。s1は3番セクションのオフセット0番地に、s2は3番セクョンのオフセット4番地に、それぞれのバイト実体が作成される。
  3. メモリ上の実体のサイズは、.symtabのSizeで示される。s1,s2とも、メモリ上の実体のサイズは4バイト。

3番セクションとは、.dataセクションである。int型変数x2の領域を確保するため、セクションのSizeは8バイトとなっている。

$ readelf -S main.o
There are 21 section headers, starting at offset 0x348:
Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 2] .rel.text         REL             00000000 0007b4 000010 08     19   1  4
  [ 3] .data             PROGBITS        00000000 000054 000008 00  WA  0   0  4

.dataセクションをバイナリダンプすると、確かにソースコード上で指定した値で初期化されていることが分かる。

$ readelf -x 3 main.o
Hex dump of section '.data':
  0x00000000 ffffff7f 78563412                   ....xV4.

実行コードにおけるシンボルの再配置情報は、.rel.textで管理される。
.rel.textで再配置情報を確認すると、

  1. .textセクションのオフセット0x5バイト目に、.dataセクションにバイト実体を持つシンボルのアドレスが展開される。
  2. .textセクションのオフセット0xfバイト目に、.dataセクションにバイト実体を持つシンボルのアドレスが展開される。
$ readelf -r main.o
Relocation section '.rel.text' at offset 0x7b4 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000005  00000301 R_386_32          00000000   .data
0000000f  00000301 R_386_32          00000000   .data
$ readelf -x 2 main.o
Hex dump of section '.rel.text':
  0x00000000 05000000 01030000 0f000000 01030000 ................

再配置されるシンボルはs1,s2であるが、上記の再配置テーブルではシンボルではなく、セクションと紐づけられている(ファイルローカルのシンボルの場合、シンボル(.symtab内でのゼロベースのエントリ番号)ではなく、セクション(ゼロベースのセクション番号)が設定される)。このままでは、.dataセクション上のどのバイト実体と紐づけられるべきなのかが分からない。

再配置エントリが、どのシンボル(.dataセクション内のバイト実体)に関連するかの情報は、再配置前の実行コードに直書きされている。その実行コード上の場所は、.rel.textセクションのOffset(.textセクション先頭からのオフセット)によって示されている。具体的には、以下コード内の中括弧の箇所[00 00 00 00][04 00 00 00]に該当する。それぞれ、.dataセクションの先頭からのオフセット0x00と0x04の領域に、そのバイト実体を持つことを示す。

$ objdump -d main.o
main.o:     file format elf32-i386
Disassembly of section .text:
00000000 <main>:
   0:	55                   push   %ebp
   1:	89 e5                	mov    %esp,%ebp
   3:	c7 05 [00 00 00 00] 7f movl   $0x7f7f7f7f,0x0
   a:	7f 7f 7f
   d:	c7 05 [04 00 00 00] 21 movl   $0x87654321,0x4
  14:	43 65 87
  17:	b8 00 00 00 00       mov    $0x0,%eax
  1c:	5d                   pop    %ebp
  1d:	c3                   ret

.dataセクションのオフセット(ELFファイル先頭からのオフセット)は0x54であるため、ELF先頭からのオフセットで表現すると、実行コード内の[00 00 00 00]は値0x54(0x54+0x00)に、[04 00 00 00]は値0x58(0x54+0x04)に、それぞれ読み替えられる。

以上をまとめると、

  1. 実行コードにおけるファイルローカル変数の再配置情報は、.rel.textで一元管理される。
  2. ファイルローカル変数の場合、再配置情報には、該当シンボル番号ではなく該当セクション番号が記載される。
  3. 該当セクション内のバイト実体を特定するためには、実行コード内に埋め込まれたオフセットを参照する。これは、該当セクションをベースとするオフセットである。

【汎整数拡張】第6回 対応表

処理系による。

数学的値 内部表現 汎整数拡張後
char -1 0xFF 0xFFFFFFFF
unsigned char 255 0xFF 0x000000FF
int -1 0xFFFFFFFF
unsigned int 4294967295 0xFFFFFFFF

比較関係

左辺↓右辺→ (char)-1 (unsigned char)255 (int)-1 (unsigned int)4294967295
(char)-1 == == ==
(unsigned char)255 > == >
(int)-1 == == ==
(unsigned int)4294967295 == > == ==

データ型モデル・規格Cごとの接尾語なし10進定数の型の違い

個人的メモです。

データ型モデル

char short int long long long void *
ILP32 8 16 32 32 64 64
LP64 8 16 32 64 64 64

規格Cごとの接尾語なしの10進定数の型について。
以下のうち、その定数がオーバーフローしないような最小の型が選ばれる*1

C89 int, long, unsigned long
C99 int, long, long long

データ型モデル・規格Cごとの接尾語なし10進定数(「-」は動作未定義)

2の冪乗 10進定数 ILP32/C89 ILP32/C99 LP64/C89 LP64/C99
0〜2^31-1 0〜2,147,483,647 int int int int
2^31〜2^32-1 〜4,294,967,295 unsigned long*2 long long long long
2^32〜2^63-1 〜9,223,372,036,854,775,807 - long long long long
2^63〜2^64-1 〜18,446,744,073,709,551,615 - - unsigned long*3 -

*1:参考「Cリファレンスマニュアル 第5版」p28

*2:gccにおいてC90モードでコンパイル時、「this decimal constant is unsigned only in ISO C90」という警告が出力されます。「C90」(ISO C90)とは、C89(ANSI C89)の別称。

*3:2を参照。

オペランドのsigned/unsigned判定マクロ

算術変換の検証をする上で、対象の整数値がsignedかunsignedかを判定するマクロを作成してみました。

#define <limits.h>
#define IS_SIGNED(x) ( \
(( (x)>>1)&(1L<<(sizeof(x)*CHAR_BIT-1)))!= \
((~(x)>>1)&(1L<<(sizeof(x)*CHAR_BIT-1))) )

というものです。

このマクロのからくりですが…

  1. 対象の整数について、その最上位ビットが「0」の場合と「1」場合の、それぞれの内部表現(ビット列)を作成します。
  2. 最上位ビットを反転させた値を作るのに、単純にビット反転しています(最上位ビット以外のビットには用が無いため)。
  3. 最上位ビットが「0」の場合と「1」の場合、それぞれの場合において、1ビット左シフトした時に空きビットにセットされた値を比較し、相違があれば算術シフトが発生→signed型、一致すれば論理シフトが発生→unsigned型…という判定を行っています。
  4. 上の理由から、「負の整数値の右ビットシフトにおいて算術シフトが発生する」処理系にのみ通用するマクロとなっています。
  5. また、char型、unsigned char型の値には対応していません(値の評価時にint型へ拡張されるため)。

2^n-1のマスク値を作成するのに、(1L<<(sizeof(x)*CHAR_BIT-1))として定数リテラルを「1L」と記述しているのは、「1」だとint型として扱われるためです(int型として扱われると不都合なのは、64ビット左シフト時に、gccから「左シフトしすぎ!」と警告を出されるためです)。

■2014/06/26 追記
こんな大掛かりなことしなくても、

#define IS_SIGNED(x) ((x)<0 || ~(x)<0)

これでいけました(汗)。

【汎整数拡張】第5回 算術シフトと論理シフト

ついに来ました、ビットシフト。みんな大好きビットシフト。

signed型の負数に対する右ビットシフトの結果は処理系依存なので、以下の話はgcc限定ととらえて下さい。

まずは、unsigned char型のビットシフトから。
unsigned型のビットシフトは解説するところが無いほど簡単です。

-----------------------------------
 unsigned char 0x7Fの左4ビットシフト
-----------------------------------
> unsigned char : 127(0x7F)
0111 1111
> unsigned char -> int(汎整数拡張)
0000 0000 0000 0000 0000 0000 0111 1111
> uc<<4
0000 0000 0000 0000 0000 0111 1111 0000

-----------------------------------
 unsigned char 0x80の左4ビットシフト
-----------------------------------
> unsigned char : 128(0x80)
1000 0000
> unsigned char -> int(汎整数拡張)
0000 0000 0000 0000 0000 0000 1000 0000
> uc<<4
0000 0000 0000 0000 0000 1000 0000 0000

-----------------------------------
 unsigned char 0x7Fの右4ビットシフト
-----------------------------------
> unsigned char : 127(0x7F)
0111 1111
> unsigned char -> int(汎整数拡張)
0000 0000 0000 0000 0000 0000 0111 1111
> uc>>4
0000 0000 0000 0000 0000 0000 0000 0111

-----------------------------------
 unsigned char 0x80の右4ビットシフト
-----------------------------------
> unsigned char : 128(0x80)
1000 0000
> unsigned char -> int(汎整数拡張)
0000 0000 0000 0000 0000 0000 1000 0000
> uc>>4
0000 0000 0000 0000 0000 0000 0000 1000

というように、空いたビットを0で埋める。それだけです。

つぎに、char型のビットシフトについて。

-----------------------------------
 char 0x7Fの左4ビットシフト
-----------------------------------
> char : 127(0x7F)
0111 1111
> char -> int(汎整数拡張)
0000 0000 0000 0000 0000 0000 0111 1111
> c<<4
0000 0000 0000 0000 0000 0111 1111 0000

-----------------------------------
 char 0x80の左4ビットシフト
-----------------------------------
> char : 128(0x80)
1000 0000
> char -> int(汎整数拡張)
1111 1111 1111 1111 1111 1111 1000 0000
> c<<4
1111 1111 1111 1111 1111 1000 0000 0000

-----------------------------------
 char 0x7Fの右4ビットシフト
-----------------------------------
> char : 127(0x7F)
0111 1111
> char -> int(汎整数拡張)
0000 0000 0000 0000 0000 0000 0111 1111
> c>>4
0000 0000 0000 0000 0000 0000 0000 0111

-----------------------------------
 char 0x80の右4ビットシフト
-----------------------------------
> char : 128(0x80)
1000 0000
> char -> int(汎整数拡張)
1111 1111 1111 1111 1111 1111 1000 0000
> c>>4
1111 1111 1111 1111 1111 1111 1111 1000

signed型の左ビットシフトの場合、空いたビットは0で埋められます(unsigned char型の場合と同じ)。右ビットシフトの場合、空いたビットは符号ビットと同じビットで埋められます。

ビットシフトによって空いたビットを何の値で埋めるか、というのをまとめると、

  左ビットシフト 右ビットシフト
char型 0 符号ビット
unsigned char型 0 0

となります。

ビットシフトにより発生した空きビットが0で埋められる場合は「論理シフト」、空きビットが符号ビットと同じビットで埋められる場合は「算術シフト」と言います。前者は符号の影響を受けず、単純にビットイメージの操作となるため「論理」、後者は符号の影響を受けるため「算術」、と覚えると覚えやすいです。

(1)signed型の右ビットシフトは算術シフトが発生する(gcc4.4.7)ため符号に注意することと、(2)式中での評価時に汎整数拡張が行われること。この2点に注意すれば、もう大丈夫ですね。あと繰り返しになりますが、signed型の負数について、右ビットシフトにより論理シフトが発生するか算術シフトが発生するかは処理系依存なので、実際の挙動については、必ずコンパイラの仕様を確認するようにして下さい。

【汎整数拡張】第4回 (2^n)による剰余とビットマスク(2^n)-1によるAND演算に等価性はあるか

整数型の値に対して、その下位nビットを取り出す場合、

x & ((1<<n)-1)

と書いたりしますが、これって2を法とする剰余(2^nで割った余り)でも表現できないか?と考えたことはあると思います。

まず、char/unsigned char型変数の値127に対して、(2^n)-1でマスクをかけてみます。

> char : 127
0111 1111
> char -> int
0000 0000 0000 0000 0000 0000 0111 1111
> c & 0x0F
0000 0000 0000 0000 0000 0000 0000 1111

> unsigned char : 127
0111 1111
> unsigned char -> int
0000 0000 0000 0000 0000 0000 0111 1111
> uc & 0x0F
0000 0000 0000 0000 0000 0000 0000 1111

と、確かに下位4ビット以外のビットがゼロクリアされています。
(途中でchar型の値が32ビットで表示されていますが、char型が式として評価されることによる汎整数拡張を示しています)

では、最上位ビットが1である場合はどうでしょうか。
char型の-1、unsigned char型の255に対して、同じく(2^n)-1でマスクをかけてみます。

> char : -1
1111 1111
> char -> int
1111 1111 1111 1111 1111 1111 1111 1111
> c & 0x0000000F
0000 0000 0000 0000 0000 0000 0000 1111

> unsigned char : 128
1111 1111
> unsigned char -> int
0000 0000 0000 0000 0000 0000 1111 1111
> uc & 0x0000000F
0000 0000 0000 0000 0000 0000 0000 1111

当然の結果ですが、確かに下位4ビット以外のビットがゼロクリアされています。

つぎに、「剰余演算子を使って下位nビットのビットマスクを再現する」ことを実験してみます。
まず、最上位ビット(符号ビット)が0の場合。

> char : 127
0111 1111
> char -> int
0000 0000 0000 0000 0000 0000 0111 1111
> c % 0x00000010
0000 0000 0000 0000 0000 0000 0000 1111

> unsigned char : 127
0111 1111
> unsigned char -> int
0000 0000 0000 0000 0000 0000 0111 1111
> uc % 0x00000010
0000 0000 0000 0000 0000 0000 0000 1111

確かに、下位4ビット以外のビットがゼロクリアされています。
ここまでは、ビットマスクと剰余演算子とには等価性があると言えそうです。

では、最上位ビット(符号ビット)が1の場合。

> char : -1
1111 1111
> char -> int
1111 1111 1111 1111 1111 1111 1111 1111 …(1)
> c % 0x00000010
1111 1111 1111 1111 1111 1111 1111 1111 …(2)

> unsigned char : 255
1111 1111
> unsigned char -> int
0000 0000 0000 0000 0000 0000 1111 1111
> uc % 0x00000010
0000 0000 0000 0000 0000 0000 0000 1111

元のデータ型がunsigned char型の場合は、想定した通りに下位4ビット以外のビットがゼロクリアされていますが、char型の場合(1)→(2)の演算では変化が起きていません。

これは言う間でもないですが…
まず、式の中でchar型変数の値が汎整数拡張によりint型に型昇格した時点で、最上位の符号ビットは1となります(1)。char型→int型の型昇格においては、空きビットには符号ビットと同じビットがセットされます。

そして、(1)の値に対して0x00000010で剰余を求める(2)のですが、まず、2^nによる剰余を求める演算とは何か、について考えてみます。

2^nによる剰余を求める演算というのは、ビット表現で言うと「下位nビット以外のすべての上位ビットを符号ビットでクリアする」ということになります。なので、(1)の値は符号ビットが1ですが、5ビット目以上のすべてのビットはもともと「1」であるため、符号ビット「1」でクリアしても、その剰余は(1)の値から変化しないことになります。

試しに、-118(0x8A)で実験してみます。

> char : 0x8A
1000 1010
> char -> int
1111 1111 1111 1111 1111 1111 1000 1010
> c % 0x00000010
1111 1111 1111 1111 1111 1111 1111 1010

というように、下位4ビット(1010)が保存され、より上位のすべてのビットが、元の符号ビットと同じ「1」でクリアされています。

まとめると、

  1. signed型の値xに対する2^nによる剰余は、下位nビットを保存し、より上位のすべてのビットをxの符号ビットでクリアする。
  2. unsigned型の値xに対する2^nによる剰余は、下位nビットを保存し、より上位のすべてのビットを「0」でクリアする。これは、ビットマスク(2^n)-1によるAND演算と等価である。

と、難しく考えましたが、-118/16=7余り-6、であることからすると当然なんですけどね(0xFAを8ビットの2の補数表現として解釈すると、-6)。

【汎整数拡張】第3回 汎整数拡張の意外な落とし穴

汎整数拡張、俗にいう「暗黙のキャスト」はソースコードに出てこないところの挙動なので、C言語上級者でもなかなかに引っかかりやすいトラップだと思います。

先日、組込み系の記事で見かけた例ですが…

以下コードは、unsigned char型変数をビット反転し、その上位4ビットを取得する(ことを意図した)ものです。
元の値は0x7F(0111 1111)なので、ビット反転すると0x80(1000 0000)となり、これの上位4ビットを取得すると、0x08(0000 1000)となる…はずです。

  5     unsigned char uc = 0x7F;
  6
  7     /* ビット反転し、上位4ビットの値を取得 */
  8     printf("uc=0x%x\n", uc);
  9     uc = ~uc>>4;
 10
 11     printf("uc=0x%x\n", uc);

しかし実行結果は…

before : uc=0x7f
after  : uc=0xf8

「0x08」とはならず、「0xF8」となりました。これは、汎整数拡張によるトラップです。
ビット単位否定演算子~がついたことで、ucは式中で評価されるため、int型に型昇格されます。あとはもう…お分かりですね?

1ステップずつ追っていきます。

元の値の内部表現です。unsigned char型なので8ビットです。

> unsigned char : 0x7F
0111 1111

次が問題なのですが、式「~uc」評価時に、内部表現がなんと32ビットに伸長しています。

> ~uc
1111 1111 1111 1111 1111 1111 1000 0000

これは、式中のucがint型に型昇格したことで「0x7F」→「0x0000007F」と伸長したためです。この時点では空きビットが0詰めされているのですが、それがビット反転により「0xFFFFFF80」となります。

> ~uc>>4
1111 1111 1111 1111 1111 1111 1111 1000

「0xFFFFFF80」を4ビット右シフトしたのですが、算術シフトにより、上位の空きビットは符号ビットの1で埋められます(算術シフト、論理シフトについては、別の回でまとめる予定です)。

最後に、unsigned char型変数への代入時、2^8で割った剰余が代入されるため、ucの値は「0xF8」となります。

> uc = ~uc>>4
1111 1000

以上、おわかりいただけただろうか。これが、汎整数拡張の恐ろしいところなんですよ。

これが

> unsigned char : 0x7F
0111 1111

こうなって(unsigned char→intへの汎整数拡張)

> uc
0000 0000 0000 0000 0000 0000 0111 1111

ああなって(ビット反転)

> ~uc
1111 1111 1111 1111 1111 1111 1000 0000

こうなって(int型なので算術右シフト。空いたビットは符号ビットと同じ1で埋められる)

> ~uc>>4
1111 1111 1111 1111 1111 1111 1111 1000

こうなるわけです。

> uc=~uc>>4
1111 1000

おわかりいただけただろうか。

おそらく、このコードを書いた人は「unsigned charに対する演算なので、7Fのビット反転で最上位ビットが1になったとしても、その4ビット右シフトは論理シフトとなるため、上位の空きビットは0で埋められるはず」と考えたのだと思います。実際は、ビット反転が行われる直前にunsigned char型→int型という型昇格が発生したため、上位の空きビットは0詰めされました。それをビット反転した結果、上位の空きビットはすべて1となったわけです。対策としては、ビットマスクをかけるなどしましょう。