c言語の printf("%x\n", (char)-1) の 出力

5 min read読了の目安(約4800字

概要

printf("%x\n", (char) -1);

というコードの出力結果で悩んだので、調査した結果をまとめる。
x86_64での結果とaarch64での結果は異なることがわかり、x86_64の方は説明できそうだが、aarch64の結果はよくわからなかった。別途深追いしたい。

Ubuntu20.04.1 (x86_64)の出力結果

下記のようなコードをコンパイルし、実行した。

#include <stdio.h>

int main(void) {
  printf("%x\n", (char)-1);
  return 0;
}

結果は下記の通り。char型だけど4byte分のffffffffが出力される。

$ gcc a.c
$ ./a.out 
ffffffff

printfの%x 指定子

printf の %x 指定子の説明を改めて確認する。

https://man7.org/linux/man-pages/man3/printf.3.html
       o, u, x, X
              The unsigned int argument is converted to unsigned octal
              (o), unsigned decimal (u), or unsigned hexadecimal (x and
              X) notation.

頭に書かれているように、%x は、unsigned int 型が引数として与えられることが期待されている。[1]

ということで、printf内で、char型 から unsigned int 型への変換がなされていると思われる。

char型 から unsigned int型への変換

https://www.jpcert.or.jp/sc-rules/c-int02-c.html
jpcert に 型変換のページがあった。
こちらに下記の記載がある。[2]
新しい型で表現できない場合、新しい型が符号無し整数型であれば、
新しい型で表現しうる最大の数に1加えた数を加えることまたは減じることを、
新しい型の範囲に入るまで繰り返すことによって得られる値に変換する。

符号ありの-1 から符号無し整数型への変換のため、-1に対してunsigned int の最大値 + 1 を足した結果、最終的にunsigned int の最大値(=0xffffffff)になるようだ。

アセンブラで見る char型 から unsigned int型への変換

下記のようなコードをコンパイルし、アセンブラを確認する。

int main(void) {
  char a = -1;
  unsigned int b = (unsigned int)a;
}

gccのバージョンは下記。

$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

これをobjdump -d して、main関数を抜粋したものが下記。

0000000000001129 <main>:
    1129:	f3 0f 1e fa          	endbr64 
    112d:	55                   	push   %rbp
    112e:	48 89 e5             	mov    %rsp,%rbp
    1131:	c6 45 fb ff          	movb   $0xff,-0x5(%rbp)
    1135:	0f be 45 fb          	movsbl -0x5(%rbp),%eax
    1139:	89 45 fc             	mov    %eax,-0x4(%rbp)
    113c:	b8 00 00 00 00       	mov    $0x0,%eax
    1141:	5d                   	pop    %rbp
    1142:	c3                   	retq   
    1143:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
    114a:	00 00 00 
    114d:	0f 1f 00             	nopl   (%rax)
    

gdb で見てみる。charの-1は0xffの直値がメモリ上のアドレス-0x5(%rbp)に、最終的に-0x4(%rbp)にunsigned intに変換された値が載っていそうだ。

  1. b mainでmain関数にbreak pointを張る。
  2. runで開始し、nで逐次実行。
  3. i regでrbpの値を確認。
  4. `x/4xb (rbp-4)でメモリをダンプ。期待値は0xffffffff。
$ gcc -g a.c
$ gdb a.out
(中略)
(gdb) b main
Breakpoint 1 at 0x1129: file a.c, line 1.
(gdb) run
Starting program: /home/taka/a.out 

Breakpoint 1, main () at a.c:1
1	int main(void) {
(gdb) n
2	  char a = -1;
(gdb) n
3	  unsigned int b = (unsigned int)a;
(gdb) n
4	}
(gdb) i reg
rax            0x0                 0
rbx            0x555555555150      93824992235856
rcx            0x555555555150      93824992235856
rdx            0x7fffffffe018      140737488347160
rsi            0x7fffffffe008      140737488347144
rdi            0x1                 1
rbp            0x7fffffffdf10      0x7fffffffdf10
rsp            0x7fffffffdf10      0x7fffffffdf10
r8             0x0                 0
r9             0x7ffff7fe0e10      140737354010128
r10            0x7ffff7ffcf78      140737354125176
r11            0x206               518
r12            0x555555555040      93824992235584
r13            0x7fffffffe000      140737488347136
r14            0x0                 0
r15            0x0                 0
rip            0x555555555141      0x555555555141 <main+24>
eflags         0x246               [ PF ZF IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
--Type <RET> for more, q to quit, c to continue without paging--
gs             0x0                 0
(gdb) x/4xb 0x7fffffffdf0c
0x7fffffffdf0c:	0xff	0xff	0xff	0xff

unsigned int b = (unsigned int)a;の結果が 0xffffffff となっていることがわかった。

aarch64編

わけあって、これのaarch64版を見る必要があった。aarch64のマシンは持ってないのでqemuでお試し。
下記の記事が大変参考になった。

https://qiita.com/tetsu_koba/items/9bdcb59f912efbff3128

qemu環境構築

sudo apt install g++-aarch64-linux-gnu qemu-user-binfmt

スタティックビルド

最初に記載の下記コードをaarch64用にクロスコンパイルする。

#include <stdio.h>

int main(void) {
  printf("%x\n", (char)-1);
  return 0;
}

簡単のためにスタティックリンクして単体で動作可能なようにコンパイルする。

$ aarch64-linux-gnu-gcc -o a.out --static a.c
$ file a.out 
a.out: ELF 64-bit LSB executable, ARM aarch64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=7c4a8e0e41744f1a5302fff86e929172b253d225, for GNU/Linux 3.7.0, not stripped

出力結果(aarch64)

下記のように0xffが出力された。なんで。。。

$ ./a.out 
ff

aarch64用のobjdumpを取得し、x86_64版と同様にアセンブラを眺めたいが、今日はここまで。

脚注
  1. この場合、hhを付与することで明示的にchar型であることを指定できる。これにより1byteの出力がなされる。printf("%hhx\n", (char)-1); ↩︎

  2. 大本はCの規格書の 6.3.1.3 Signed and unsigned integers に記載されてるらしい ↩︎