[M1 Mac, Big Sur 11.6.8, g++-12 12.2.0, NO IDE]
主な将棋エンジンのApple Siliconへの対応状況を調べました。GUIアプリには”将棋所”を使って動作確認しました。
せっかくC++を学んでいるのですから、将棋エンジンを作らないまでもコードの構造ぐらいは見ておこうと思います。
[M1 Mac, Big Sur 11.6.8, g++-12 12.2.0, NO IDE]
主な将棋エンジンのApple Siliconへの対応状況を調べました。GUIアプリには”将棋所”を使って動作確認しました。
せっかくC++を学んでいるのですから、将棋エンジンを作らないまでもコードの構造ぐらいは見ておこうと思います。
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]
printfでダブルクォート、シングルクォートと、&ありなしの組み合わせにて出力してみました。
シングルクォートでは1文字を囲むことになっていますが、複数文字を囲むと全体で1文字と解釈されリトルエンディアンのルールにより下位アドレスから順に格納されるため並びが逆転します。
C言語についてはおびただしい数のネット情報がありますが、検証する必要がないにしても誰もこういったお遊びをしていませんでした。面白いネタだと思うのですが…
#include <stdio.h>
int main() {
char c1[] = "abc";
char* c2 = "def";
char* c3 = 'ghi';
printf("c1: %s\n", c1);
printf("&c1: %s\n", &c1);
printf("c2: %s\n", c2);
printf("&c2: %s\n", &c2);
// printf("c3: %s\n", c3); // Segmentation fault: 11
printf("&c3: %s\n", &c3);
return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
c1: abc
&c1: abc
c2: def
&c2: P? // ポインタをASCIIコードとして解釈し出力
&c3: ihg // ghiの並びが逆転する
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]
LLDBを使ってレジスタ値やメモリ値の変化を把握し、アセンブリ言語の仕組みを確認しました。
アセンブリ言語のコード全55行の1行ごとにregister readコマンド、memory readコマンドを実行して、レジスタ値、スタック作業領域(sp〜fp)の値を出力し、Excelに貼り付けるというなかなか手間のかかる作業でした。
まず変数は静的領域に格納され、関数の引数に使われる変数はスタック領域にコピーします。
そしてレジスタに即値かスタック領域の番地をコピーしてから、関数を呼び出します。
printfの引数でiのように&がついていないものはレジスタに即値がコピーされていて、&cのように&がついているものはスタック領域に格納されている変数の番地がレジスタにコピーされます。
&は”スタック領域にある変数の番地をレジスタにコピーする”目印だということが理解できました。
vmmapコマンドでメモリ領域の内訳等が出力できます。
#include <stdio.h>
int main() {
int i;
char c[] = {'a', 'b', 'c', 'd', 'e'};
char* c_ptr;
char* c_ptr2;
i = 1000;
c_ptr = 'xyz';
c_ptr2 = "stu"; // ダブルクォートではstuが格納されている静的領域メモリ番地がスタック領域にコピーされる
printf("i: %d\n", i);
printf("c: %s\n", &c);
printf("c_ptr: %s\n", &c_ptr);
printf("c_ptr2: %s\n", &c_ptr2);
return 0;
}
test`main:
0x100003e48 <+0>: sub sp, sp, #0x60 ; =0x60
0x100003e4c <+4>: stp x29, x30, [sp, #0x50]
0x100003e50 <+8>: mov w8, #0x0
0x100003e54 <+12>: str w8, [sp, #0x2c]
0x100003e58 <+16>: str wzr, [sp, #0x4c]
0x100003e5c <+20>: adrp x8, 0
0x100003e60 <+24>: add x8, x8, #0xf50 ; =0xf50
0x100003e64 <+28>: ldr w9, [x8]
0x100003e68 <+32>: add x10, sp, #0x40 ; =0x40
0x100003e6c <+36>: str x10, [sp, #0x10]
0x100003e70 <+40>: str w9, [sp, #0x40]
0x100003e74 <+44>: ldrb w8, [x8, #0x4]
0x100003e78 <+48>: strb w8, [sp, #0x44]
0x100003e7c <+52>: mov w8, #0x3e8
0x100003e80 <+56>: str w8, [sp, #0x48]
0x100003e84 <+60>: add x8, sp, #0x38 ; =0x38
0x100003e88 <+64>: str x8, [sp, #0x18]
0x100003e8c <+68>: mov x8, #0x797a
0x100003e90 <+72>: movk x8, #0x78, lsl #16
0x100003e94 <+76>: str x8, [sp, #0x38]
0x100003e98 <+80>: add x8, sp, #0x30 ; =0x30
0x100003e9c <+84>: str x8, [sp, #0x20]
0x100003ea0 <+88>: adrp x8, 0
0x100003ea4 <+92>: add x8, x8, #0xf55 ; =0xf55
0x100003ea8 <+96>: str x8, [sp, #0x30]
0x100003eac <+100>: ldr w9, [sp, #0x48]
0x100003eb0 <+104>: mov x8, x9
0x100003eb4 <+108>: adrp x0, 0
0x100003eb8 <+112>: add x0, x0, #0xf59 ; =0xf59
0x100003ebc <+116>: mov x9, sp
0x100003ec0 <+120>: str x8, [x9]
0x100003ec4 <+124>: bl 0x100003f20 ; symbol stub for: printf
0x100003ec8 <+128>: ldr x8, [sp, #0x10]
0x100003ecc <+132>: adrp x0, 0
0x100003ed0 <+136>: add x0, x0, #0xf60 ; =0xf60
0x100003ed4 <+140>: mov x9, sp
0x100003ed8 <+144>: str x8, [x9]
0x100003edc <+148>: bl 0x100003f20 ; symbol stub for: printf
0x100003ee0 <+152>: ldr x8, [sp, #0x18]
0x100003ee4 <+156>: adrp x0, 0
0x100003ee8 <+160>: add x0, x0, #0xf67 ; =0xf67
0x100003eec <+164>: mov x9, sp
0x100003ef0 <+168>: str x8, [x9]
0x100003ef4 <+172>: bl 0x100003f20 ; symbol stub for: printf
0x100003ef8 <+176>: ldr x8, [sp, #0x20]
0x100003efc <+180>: adrp x0, 0
0x100003f00 <+184>: add x0, x0, #0xf72 ; =0xf72
0x100003f04 <+188>: mov x9, sp
0x100003f08 <+192>: str x8, [x9]
0x100003f0c <+196>: bl 0x100003f20 ; symbol stub for: printf
0x100003f10 <+200>: ldr w0, [sp, #0x2c]
0x100003f14 <+204>: ldp x29, x30, [sp, #0x50]
0x100003f18 <+208>: add sp, sp, #0x60 ; =0x60
0x100003f1c <+212>: ret
~ $ vmmap 28644
Process: test [28644]
Path: /Volumes/VOLUME/*/test
Load Address: 0x100000000
Identifier: test
Version: ???
Code Type: ARM64
Platform: macOS
Parent Process: debugserver [28645]
Date/Time: 2022-10-08 15:55:12.687 +0900
Launch Time: 2022-10-08 15:54:41.754 +0900
OS Version: macOS 11.6.1 (20G224)
Report Version: 7
Analysis Tool: /Applications/Xcode.app/Contents/Developer/usr/bin/vmmap
Analysis Tool Version: Xcode 13.2.1 (13C100)
Physical footprint: 865K
Physical footprint (peak): 865K
----
Virtual Memory Map of process 28644 (test)
Output report format: 2.4 -- 64-bit process
VM page size: 16384 bytes
==== Non-writable regions for process 28644
REGION TYPE START - END [ VSIZE RSDNT DIRTY SWAP] PRT/MAX SHRMOD PURGE REGION DETAIL
__TEXT 100000000-100004000 [ 16K 16K 16K 0K] r-x/rwx SM=COW /Volumes/VOLUME/*/test
__DATA_CONST 100004000-100008000 [ 16K 16K 16K 0K] r--/rw- SM=COW /Volumes/VOLUME/*/test
__LINKEDIT 10000c000-100010000 [ 16K 16K 0K 0K] r--/r-- SM=COW /Volumes/VOLUME/*/test
__TEXT 100010000-100024000 [ 80K 80K 0K 0K] r-x/r-x SM=COW /usr/lib/dyld
__TEXT 100024000-100028000 [ 16K 16K 16K 0K] r-x/rwx SM=COW /usr/lib/dyld
__TEXT 100028000-100090000 [ 416K 368K 0K 0K] r-x/r-x SM=COW /usr/lib/dyld
__DATA_CONST 100090000-100098000 [ 32K 16K 16K 16K] r--/rw- SM=COW /usr/lib/dyld
__LINKEDIT 1000d0000-100114000 [ 272K 240K 0K 0K] r--/r-- SM=COW /usr/lib/dyld
__LINKEDIT 100114000-100118000 [ 16K 0K 0K 0K] r--/r-- SM=NUL /usr/lib/dyld
shared memory 100120000-100124000 [ 16K 16K 16K 0K] r--/r-- SM=SHM
MALLOC metadata 100124000-100128000 [ 16K 16K 16K 0K] r--/rwx SM=ZER ...cZone_0x100124000 zone structure
MALLOC guard page 10012c000-100130000 [ 16K 0K 0K 0K] ---/rwx SM=ZER
MALLOC guard page 100138000-10013c000 [ 16K 0K 0K 0K] ---/rwx SM=ZER
MALLOC guard page 10013c000-100140000 [ 16K 0K 0K 0K] ---/rwx SM=NUL
MALLOC guard page 100148000-100150000 [ 32K 0K 0K 0K] ---/rwx SM=NUL
MALLOC guard page 100158000-10015c000 [ 16K 0K 0K 0K] ---/rwx SM=NUL
MALLOC metadata 10015c000-100160000 [ 16K 16K 16K 0K] r--/rwx SM=PRV
STACK GUARD 16be00000-16f604000 [ 56.0M 0K 0K 0K] ---/rwx SM=NUL stack guard for thread 0
__TEXT 18e5be000-18e5c0000 [ 8K 8K 0K 0K] r-x/r-x SM=COW ...ib/system/libsystem_blocks.dylib
__TEXT 18e5c0000-18e5f8000 [ 224K 224K 0K 0K] r-x/r-x SM=COW /usr/lib/system/libxpc.dylib
__TEXT 18e5f8000-18e610000 [ 96K 96K 0K 0K] r-x/r-x SM=COW ...lib/system/libsystem_trace.dylib
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]
アセンブル言語のコードを解読するため、LLDBを使ってレジスタ値の変化を表にまとめました。黄色のセルで値が変わっています。
register readコマンドでmain関数先頭行でのレジスタ値を出力し、nextコマンドで1行ずつ進めて都度レジスタ値を出力しました。
#include <stdio.h>
int main() {
int i = 1000;
char c[6] = {'a', 'b', 'c', 'd', 'e'};
char* c_ptr = 'xyz';
char* c_ptr2 = "stu"; // あえてダブルクォートを使用
printf("i: %d\n", i);
printf("c: %s\n", &c);
printf("c_ptr: %s\n", &c_ptr);
printf("c_ptr2: %s\n", &c_ptr2);
return 0;
}
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]
# LLDB # コマンド
LLDBを使って変数のアドレスを調べています。
ゆくゆくはアセンブリ言語で簡単なコードを書くつもりなので、C++ではなくC言語で書いていきます。
今回はchar、char*について調べました。ポインタ自身のアドレスがわからなかったのですが、memory readコマンドの結果を照らし合わせて突き止めることができました。
下図の青線で囲んだところがポインタのようです。メモリアドレスが書かれています。変数にほぼ隣接した下位側に位置しています。
#include <stdio.h>
int main() {
int i = 1000;
char c[6] = {'a', 'b', 'c', 'd', 'e'};
char* c_ptr = 'xyz';
printf("i: %d\n", i);
printf("c: %s\n", &c);
printf("c_ptr: %s\n", &c_ptr);
return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
i: 1000
c: abcde
c_ptr: zyx // なぜ逆さになるのかは不明
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]
変数の格納アドレスを検証していきます。
LLDBを使って確認しました。
#include <stdio.h>
#include <iostream>
#include <string>
using std::cout; using std::endl;
using std::to_string;
int main() {
int i = 256;
char c[6] = {'a', 'b', 'c', 'd', 'e'};
std::string str("abcde");
cout << "iのアドレス: " << &i << endl;
cout << "iのサイズ: " << to_string(sizeof i) << endl;
cout << "cのアドレス: " << &c << endl;
cout << "cのサイズ: " << to_string(sizeof c) << endl;
cout << "strのアドレス: " << &str << endl;
cout << "strのサイズ: " << to_string(sizeof str) << endl;
return 0;
}
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]
引き続き、バッファオーバーフロー対策であるclang++の-fstack-protector-allオプションについて実用性を検証しました。
前回得られたデータから、char変数を増やしていくとやがてint変数のメモリ領域を上書きしてしまうと予想しましたが、実際にそうなるのか検証しました。
変数i予想値の算出方法:
2進数 1101000 00000000 00000000 00000000
反転 0010111 11111111 11111111 11111111
1加算 0011000 00000000 00000000 00000000
10進数 -402653184
その結果、char変数の数に関係なく、int変数との間に複数の空アドレスが配置されることが判明しました。
さすがに文法さえ正しければコードは正常に動くようです。
scanf関数の検証は今度こそこれで終わりにします。
#include <cppstd.h>
int main() {
int i;
char c[2];
char c2[2];
char c3[2];
char c4[2];
char c5[2];
char c6[2];
for (i=0; i< 10; i++){
printf("for文先頭直後 printf i: %d\n",i);
// rewind(stdin); // ストリームバッファをクリア
scanf("%s", &c);
cout << "cout c: " << c << endl;
printf("printf c: %s\n",&c);
scanf("%s", &c2);
cout << "cout c2: " << c2 << endl;
printf("printf c2: %s\n",&c2);
scanf("%s", &c3);
cout << "cout c3: " << c3 << endl;
printf("printf c3: %s\n",&c3);
scanf("%s", &c4);
cout << "cout c4: " << c4 << endl;
printf("printf c4: %s\n",&c4);
scanf("%s", &c5);
cout << "cout c5: " << c5 << endl;
printf("printf c5: %s\n",&c5);
scanf("%s", &c6);
cout << "cout c6: " << c6 << endl;
printf("printf c6: %s\n",&c6);
printf("printf i: %d\n",i);
cout << "for文終端" << endl;
}
return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
for文先頭直後 printf i: 0
a
cout c: a
printf c: a
b
cout c2: b
printf c2: b
c
cout c3: c
printf c3: c
d
cout c4: d
printf c4: d
e
cout c5: e
printf c5: e
f
cout c6: f
printf c6: f
printf i: 0
for文終端
for文先頭直後 printf i: 1
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]
バッファオーバーフロー対策であるclang++の-fstack-protector-allオプションについて実用性を検証しました。
その結果、char変数を複数宣言した場合、その間には空白アドレスが配置されずメモリアドレスの干渉が起こることが判明しました。
配列の文字数を正しく宣言すればいいのかというとそうでもなくて、char変数の数を増やすと下位アドレス側にメモリアドレスが伸びていき、ついにはint変数のメモリ領域が上書きされる可能性があります。
この程度で破綻するようでは、実用性は低いと判断するほかないです。このオプションは使わずに、char配列の文字数を正確に宣言するのが最善かと思います。
そもそも文法的に正しいC言語を書けば問題ないのであって、下手に補完して我々プログラマを甘やかすと余計に状況が悪くなるといった感じがします。
#include <cppstd.h>
int main() {
int i;
char c[2]; // ここを変えて検証
char c2[2]; // ここを変えて検証
for (i=0;i<5;i++){
printf("for文先頭直後 printf i: %d\n",i);
// rewind(stdin); // ストリームバッファをクリア
scanf("%s", &c);
cout << "cout c: " << c << endl;
printf("printf c: %s\n",&c);
scanf("%s", &c2);
cout << "cout c2: " << c2 << endl;
printf("printf c2: %s\n",&c2);
cout << "2回目cout c: " << c << endl;
printf("2回目printf c: %s\n",&c);
printf("printf i: %d\n",i);
}
return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
for文先頭直後 printf i: 0
a
cout c: a
printf c: a
b
cout c2: b
printf c2: b
2回目cout c: a
2回目printf c: a
printf i: 0
for文先頭直後 printf i: 1
a
cout c: a
printf c: a
b
cout c2: b
printf c2: b
2回目cout c: a
2回目printf c: a
printf i: 1
for文先頭直後 printf i: 2
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]
前回までscanf関数におけるバッファオーバーフローについて検証してきました。
コンパイラの対策としてgcc, g++, clang++などにはバッファオーバーフロー検知のメカニズムが実装されていて、未然に処置を施す仕組みになっています。
この機能はコンパイル時に-fstack-protector-allオプションを付加すると有効になります。
LLDBでその挙動を確認したところ、オーバーフローが発生しそうなchar変数をより上位アドレスに配置し、本来なら隣接するはずの別変数との位置関係が逆転してさらにその間に10バイトの空白アドレスが配置されました。
気をつけないといけないのはプログラムにてprintfなどでこれらの変数のメモリアドレスを出力させるとこの機能が無効になってしまう点です。
#include <cppstd.h>
int main() {
int i;
char c;
for (i=0;i<5;i++){
printf("for文先頭直後 printf i: %d\n",i);
// rewind(stdin); // ストリームバッファをクリア
scanf("%s", &c);
cout << "cout c: " << c << endl;
printf("printf c: %s\n",&c);
printf("printf i: %d\n",i);
}
return 0;
--------------------------------------------------
出力
--------------------------------------------------
for文先頭直後 printf i: 0
a
cout c: a
printf c: a
printf i: 0
for文先頭直後 printf i: 1
a
cout c: a
printf c: a
printf i: 1
for文先頭直後 printf i: 2
(lldb) print &i
(int *) $1 = 0x000000016fdff57c
(lldb) print &c
(char *) $2 = 0x000000016fdff587 "a"
『Cプログラミングの落とし穴』(A.コーニグ, 1990)
[M1 Mac, Big Sur 11.6.8, clang 13.0.0, NO IDE]
ようやく正常な動作の紹介にたどりつきました。
きりがないのでscanf編はこれくらいにしておきます。
#include <cppstd.h>
int main() {
int i;
char c[2];
cout << "iのアドレス: " << &i << endl;
cout << "iのサイズ: " << to_string(sizeof i) << endl;
cout << "cのアドレス: " << &c << endl;
cout << "cのサイズ: " << to_string(sizeof c) << endl;
for (i=0;i<5;i++){
printf("for文先頭直後 printf i: %d\n",i);
// rewind(stdin); // ストリームバッファをクリア
scanf("%s", &c);
cout << "cout c: " << c << endl;
printf("printf c: %s\n",&c);
printf("printf i: %d\n",i);
}
return 0;
}
--------------------------------------------------
出力
--------------------------------------------------
iのアドレス: 0x16f01f698
iのサイズ: 4
cのアドレス: 0x16f01f696
cのサイズ: 2
for文先頭直後 printf i: 0
a
cout c: a
printf c: a
printf i: 0
for文先頭直後 printf i: 1
a
cout c: a
printf c: a
printf i: 1
for文先頭直後 printf i: 2