未定義動作に初めて当たった話

## この記事と私について
　この記事はKCS Advent Calender 2024の7日目の記事です。
　お初にお目にかかります、きんべんと申します。最近はFlutterを用いたAndroidアプリ開発を行っています。今回はそれとは全く関係なく、あるCの課題にて直面した現象について話していきたいと思います。何か知っていることや、間違っていることがあればコメントしていただけると幸いです。
 
## コンパイル対象のコードについて
　悩みの発端となった課題は、以下のコードをO0でコンパイルした場合とO2でコンパイルした場合の結果を比べてみて、その違いの原因を考察するというものでした。
```c:pra3.c
#include <stdio.h>

int a = 10;
int b = 20;
int c = 30;

int main(){
    int *p = &b;
    *(p-1) = 11;
    *(p+1) = 33;
    printf("a = %d, b = %d, c = %d\n",a,b,c);
    return 0;
}
```
　仮に、このコードがそのまま実行されたとすると、グローバル変数の`a`と`c`はそれぞれメモリ上で変数`b`の4バイト上、4バイト下に存在するので、`a`と`c`の値はそれぞれ代入された値が出力されることになります。実際にこのコードをO0、O2でコンパイルした場合の実行結果はそれぞれこうなりました。
```
a = 11, b = 20, c = 33 //-O0
a = 10, b = 20, c = 30 //-O2
```
　なんと、O2では`a`と`c`の変化前の値が出力されました。この原因をどうにかして探りたかったのですが、どうやらgccにはアセンブリを取得するオプションがあるそうです。それぞれのアセンブリのmain以下を示します。
#### O0
```
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp	#
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp	#,
	.cfi_def_cfa_register 6
	subq	$16, %rsp	#,
# pra3.c:8:     int *p = &b;
	movq	$b, -8(%rbp)	#, p
# pra3.c:9:     *(p-1) = 11;
	movq	-8(%rbp), %rax	# p, tmp94
	subq	$4, %rax	#, _1
# pra3.c:9:     *(p-1) = 11;
	movl	$11, (%rax)	#, *_1
# pra3.c:10:     *(p+1) = 33;
	movq	-8(%rbp), %rax	# p, tmp95
	addq	$4, %rax	#, _2
# pra3.c:10:     *(p+1) = 33;
	movl	$33, (%rax)	#, *_2
# pra3.c:11:     printf("a = %d, b = %d, c = %d\n",a,b,c);
	movl	c(%rip), %ecx	# c, c.0_3
	movl	b(%rip), %edx	# b, b.1_4
	movl	a(%rip), %eax	# a, a.2_5
	movl	%eax, %esi	# a.2_5,
	movl	$.LC0, %edi	#,
	movl	$0, %eax	#,
	call	printf	#
# pra3.c:12:     return 0;
	movl	$0, %eax	#, _11
# pra3.c:13: }
	leave	
	.cfi_def_cfa 7, 8
	ret	
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (GNU) 8.5.0 20210514 (Red Hat 8.5.0-22)"
	.section	.note.GNU-stack,"",@progbits
```
#### O2
```
main:
.LFB11:
	.cfi_startproc
	subq	$8, %rsp	#,
	.cfi_def_cfa_offset 16
# pra3.c:11:     printf("a = %d, b = %d, c = %d\n",a,b,c);
	movl	c(%rip), %ecx	# c,
	movl	$.LC0, %edi	#,
	xorl	%eax, %eax	#
	movl	b(%rip), %edx	# b,
	movl	a(%rip), %esi	# a,
# pra3.c:9:     *(p-1) = 11;
	movl	$11, b-4(%rip)	#, MEM[(int *)&b + -4B]
# pra3.c:10:     *(p+1) = 33;
	movl	$33, b+4(%rip)	#, MEM[(int *)&b + 4B]
# pra3.c:11:     printf("a = %d, b = %d, c = %d\n",a,b,c);
	call	printf	#
# pra3.c:13: }
	xorl	%eax, %eax	#
	addq	$8, %rsp	#,
	.cfi_def_cfa_offset 8
	ret	
	.cfi_endproc
.LFE11:
	.size	main, .-main
	.globl	c
	.data
	.align 4
	.type	c, @object
	.size	c, 4
c:
	.long	30
	.globl	b
	.align 4
	.type	b, @object
	.size	b, 4
b:
	.long	20
	.globl	a
	.align 4
	.type	a, @object
	.size	a, 4
a:
	.long	10
	.ident	"GCC: (GNU) 8.5.0 20210514 (Red Hat 8.5.0-22)"
	.section	.note.GNU-stack,"",@progbits
```
　アセンブリを一行ずつ解説することはしませんが、最も重要なのはO2のアセンブリにおいて`printf`の実行が二つのアセンブリコード群に分かれていることです。後半は`printf`関数を呼ぶだけなのでいいとして、前半は何をやっているのかというとレジスタにメモリ上の変数の値を移動させています。これにより、標準出力にて値を表示できるということですね(実際は`printf`関数の命令を見ていないので、どのようにレジスタの値がI/Oに繋がれているかはわからない)。このレジスタへの値の移動のタイミングがメモリへの値の書き込みより前に行われた結果、出力される値が異なっているんですね。
　課題への回答としてはこれで十分なのですが、私にとってはここからが本番でした。なぜ、レジスタへの値の書き込みを`printf`の呼び出しより先に行うのだろう？
 
## 変なコードの正体
　さて、ここまでを読んで、既にご存知の方にとっては当然のことだと思うのですが、C言語には __未定義動作__ というものが存在します。この未定義動作とは、規格書によって定められていない動作のことで、これにあたる部分をコードに見つけると、コンパイラは結果の保証をしません。それでは、今回はどの部分が未定義動作になっているかというと、以下の二行、特にその左辺があたります。
````
*(p-1) = 11;
*(p+1) = 33;
````
今回のメモリ番地としては問題がないように思えますが、このようなアクセスをする場合、`p-1`と`p+1`は少なくとも配列の要素として存在している必要があります[^1]。ところが、`p`は`b`のアドレスを指していてそもそも配列になっておらず、これらは範囲外を参照していることになります。これが原因で、最適化の結果`printf`より後回しにされたのだと思います。

[^1]:https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf 6.5.6 paragraph 8

## おわりに
　以上、実行結果が異なる原因について調べたところ、その原因は未定義動作だった、ということでした。未定義動作というものにあたるのは私としては初めてで、友人に聞くまではそもそもこの単語のことを知りませんでした。C言語は適当に書いてもそのまま実行してくれるものだと思っていたので、こういったケースには気を付けたいと思います。
　ある問題に対して深く考えることで、アセンブリの取得からそのちょっとした解析までできたのは非常にワクワクして楽しかったので、自らの良い経験としてここに記しておきたいと思います。未定義動作について教えてくれた[lemola](https://github.com/lemolatoon)くんをはじめ、初めて記事を書くにあたり協力してくれた複数の友人に感謝したいと思います。