C言語のコンパイラの最適化でプログラムが正常に動作しないんだけど...
C言語からアセンブリ言語への変換箇所を特定する方法を教えて!
こういった悩みにお答えします.
本記事の信頼性
- リアルタイムシステムの研究歴12年.
- 東大教員の時に,英語でOS(Linuxカーネル)の授業.
- 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
- プログラミング歴15年以上,習得している言語: C/C++,Python,Solidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Verse(UEFN), Assembler (x64,ARM).
- 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」をGitHubにオープンソースとして公開.
- 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
- 最近は自然言語処理AIとイーサリアムに関する有益な情報発信に従事.
- (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
- (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.
こういった私から学べます.
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!
目次
C言語のコンパイラの最適化による不具合
C言語のコンパイラ(主にGCC)の最適化でプログラムが正常に動作しなかった経験がありませんか?
コンパイラの最適化は,プログラムの実行速度を向上するメリットがありますが,コードが正常に動作しなくなることがあるデメリットがあります.
もちろん,コンパイラの最適化を抑制するために,型修飾子のvolatileを入れることも大事です.
その時に,C言語の処理が正しくアセンブリ言語に変換されているかどうかチェックしたいですよね.
そんなあなたにおすすめするC言語からアセンブリ言語への変換箇所を特定する方法を教えます.
C言語歴15年以上の私が実際に使っている方法ですので,是非習得しましょう!
C言語からアセンブリ言語への変換箇所を特定する方法
C言語からアセンブリ言語への変換箇所を特定する方法はasm volatile("label:")を挿入することです.
ここで,asm volatile("label:")はアセンブリ言語をC言語の中で利用できるGCC拡張です.
Visual Studioでは利用できないことに注意して下さい.
以下のコードのラベル名は__beginと__endとしましたが,関数名やグローバル変数名と同じ名前でなければ何でも良いです.
1 2 3 |
asm volatile("__begin:"); /* insert codes. */ asm volatile("__end:") |
ループ処理のコード例
ループ処理のコード例は以下になります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #define NR_LOOPS 5 int main(void) { int i; asm volatile("__begin:"); for (i = 0; i < NR_LOOPS; i++) { printf("i = %d\n", i); } asm volatile("__end:"); return 0; } |
実行結果は以下になります.
1 2 3 4 5 6 7 |
$ gcc c2assembly.c $ a.out i = 0 i = 1 i = 2 i = 3 i = 4 |
ディスアセンブル(「objdump -Da a.out」を実行)の結果は以下になります.
main関数の中に__beginと__endのラベルがあり,その間にあるアセンブリ言語がループ処理になっていることがわかります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
$ objdump -Da a.out a.out: file format elf64-x86-64 a.out ... # 一部省略 0000000000001149 <main>: 1149: f3 0f 1e fa endbr64 114d: 55 push %rbp 114e: 48 89 e5 mov %rsp,%rbp 1151: 48 83 ec 10 sub $0x10,%rsp 0000000000001155 <__begin>: 1155: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 115c: eb 1a jmp 1178 <__begin+0x23> 115e: 8b 45 fc mov -0x4(%rbp),%eax 1161: 89 c6 mov %eax,%esi 1163: 48 8d 3d 9a 0e 00 00 lea 0xe9a(%rip),%rdi # 2004 <_IO_stdin_used+0x4> 116a: b8 00 00 00 00 mov $0x0,%eax 116f: e8 dc fe ff ff callq 1050 <printf@plt> 1174: 83 45 fc 01 addl $0x1,-0x4(%rbp) 1178: 83 7d fc 04 cmpl $0x4,-0x4(%rbp) 117c: 7e e0 jle 115e <__begin+0x9> 000000000000117e <__end>: 117e: b8 00 00 00 00 mov $0x0,%eax 1183: c9 leaveq 1184: c3 retq 1185: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 118c: 00 00 00 118f: 90 ... # 一部省略 |
コンパイラの最適化の注意点
asm volatile("label:")を利用する場合におけるコンパイラの最適化の注意点を説明します.
ループ処理のコード例(中身なし)は以下になります.
for文の中身がないこと以外は前回のコードと同じです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #define NR_LOOPS 5 int main(void) { int i; asm volatile("__begin:"); for (i = 0; i < NR_LOOPS; i++) { } asm volatile("__end:"); return 0; } |
コンパイラの最適化なしのディスアセンブルの結果
コンパイラの最適化なしの実行結果は以下になります.
もちろん何も出力されません.
1 2 3 |
$ gcc c2assembly2.c $ a.out $ |
ディスアセンブルの結果は以下になります.
main関数の中に__beginと__endのラベルがあることがわかります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
$ objdump -Da a.out a.out: file format elf64-x86-64 a.out ... # 一部省略 0000000000001129 <main>: 1129: f3 0f 1e fa endbr64 112d: 55 push %rbp 112e: 48 89 e5 mov %rsp,%rbp 0000000000001131 <__begin>: 1131: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 1138: eb 04 jmp 113e <__begin+0xd> 113a: 83 45 fc 01 addl $0x1,-0x4(%rbp) 113e: 83 7d fc 04 cmpl $0x4,-0x4(%rbp) 1142: 7e f6 jle 113a <__begin+0x9> 0000000000001144 <__end>: 1144: b8 00 00 00 00 mov $0x0,%eax 1149: 5d pop %rbp 114a: c3 retq 114b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) ... # 一部省略 |
コンパイラの最適化ありのディスアセンブルの結果
コンパイラの最適化あり(-O2オプション)の実行結果は以下になります.
同様に何も出力されません.
1 2 3 |
$ gcc c2assembly2.c -O2 $ a.out $ |
ディスアセンブルの結果は以下になります.
main関数の中に__beginラベルはありますが,__endラベルがコンパイラの最適化でなくなっていて,__startラベルが配置されています.
また,コンパイラの最適化なしの場合と比較すると,__beginの中身も変更されています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ objdump -Da a.out a.out: file format elf64-x86-64 a.out ... # 一部省略 0000000000001040 <main>: 1040: f3 0f 1e fa endbr64 0000000000001044 <__begin>: 1044: 31 c0 xor %eax,%eax 1046: c3 retq 1047: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1) 104e: 00 00 0000000000001050 <_start>: 1050: f3 0f 1e fa endbr64 ... # 一部省略 |
このようにasm volatile("label:")を利用することで,コンパイラの最適化によるアセンブリ言語の変換箇所が簡単に特定できますよね.
質問:objdumpコマンドで-Sオプションの方が良いのでは?
Twitterで「objdumpコマンドで-Sオプションの方が良いのでは?」という質問を頂きました.
ご指摘の通り,objdump コマンドで-Sオプションを利用するとコードを表示することはできます.
もちろん,この方法もおすすめできますが,asm volatile("label:")は自分が知りたい特定の場所のみをピンポイントで知りたい場合に効果的です.
両方の良いところをうまく使い分けると良いと思います.
まとめ
C言語からアセンブリ言語への変換箇所を特定する方法としてGCC拡張のasm volatile("label:")を紹介しました.
asm volatile("label:")を利用することで,コンパイラの最適化による処理の変換箇所を見つけることが簡単になります.
とても実用的なので,早速あなたのコードで使ってみましょう!
C言語を独学で習得することは難しいです.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!