MacOS用アセンブリへの変更点

2019-11-23

自作のC言語は64bit Linux上で動かすことを前提としているが、他のプラットフォームでも動くのであればなおよい。 以前はコンパイラから直接実行形式のバイナリを出力していたので難しかったけど、いったんアセンブリを出力してそれをアセンブルするように変更したので、 ELFを出力する自作のアセンブラじゃなくgcc(clang)を呼び出すようにすればMac上でも動かせるんじゃないか、とやってみた。

ソースの修正

環境の判別

環境依存で動作を変更したい場合、C言語では定義済みマクロで判定する。 動かしているのがMac上の場合には __APPLE__ が定義されているので、それを使用する:

#if defined(__linux__)
...
#elif defined(__APPLE__)
...
#endif

標準関数が未定義になってしまうのを回避

Mac上でソースをコンパイルすると snprintf が未定義でコンパイルに失敗してしまう。 そんなことあるか?と思ってヘッダファイル /usr/include/stdio.h を見てみる:

#if __DARWIN_C_LEVEL >= 200112L || defined(_C99_SOURCE) || defined(__cplusplus)
__BEGIN_DECLS
int snprintf(char * __restrict __str, size_t __size, const char * __restrict __format, ...) __printflike(3, 4);
...

などと #if で定義が囲まれている。 テスト用のコードを作成して調べたところ __DARWIN_C_LEVEL900000L だった。

これなら条件は有効そうだが…と思ったら、 _POSIX_SOURCE を定義していたのが原因だった (_POSIX_SOURCE は、Linuxで kill が未定義になるのを解決するためにMakefile内で定義していた)。 _POSIX_SOURCE を定義していなければ __DARWIN_C_LEVEL900000L(_DARWIN_C_FULL) だが、定義していると _POSIX_C_SOURCE を参照するようになっていて、 _POSIX_C_SOURCE198808L となってしまうようだ(<sys/cdefs.h>)。

_POSIX_SOURCE をMakefile内で一律に定義するんじゃなく、 kill を使用する箇所でLinux の場合にのみ定義するようにして回避する。

出力するコードの修正

Linux上のgccとMac上のclangが受け付けるアセンブリの形式に違いがあって、C言語をコンパイルして出力するコードをそれに合わせるようにする必要がある。

読み込み専用データセクション

C言語で const を指定した、読み込み専用で書き込み不可のデータは、Linuxでは .rodata セクションに配置する:

.section .rodata

しかしこの書式をMac上のclangアセンブラは受け付けてくれない。 __TEXT,__const にする必要がある:

.section __TEXT,__const

グローバルなシンボルにアンダースコアをつける

Linuxの場合にはC言語で定義したシンボルがそのままの名前でアセンブリに出力される。 しかしMacでは前にアンダースコア _ が追加される。 例えば write という関数を呼び出していても実際には _write というシンボルを参照することになる。

またC言語標準のエントリポイントも実際に参照されるのは main じゃなくて _main となり、C言語から出力するときにアンダースコアをつけてやらないと未定義でリンクに失敗する。

アライメントの指定方法

データを特定の数値の倍数のアドレスに配置するために、 .align というディレクティブで指定する。 これはそのままMac上でもアセンブルできるが実際には動作が違っていて、指定が合わせたいバイト数じゃなく2の何乗かという意味で解釈される (或るプログラマの一生 » Mac (Intel Mac) のアセンブラでは .align は .p2align の意味である)。 なので例えば構造体とかのアライメントが崩れて、値がおかしくなってしまう。

そこで .p2align と出力して、値も2の何乗かを出力するよう変更する。 Linux上で .p2align が使えるんであればそちらに統一でもいいんだけど、Linuxでは受け付けてくれない…。

以上の修正で、単純なコードならコンパイルして動かせるようになった。