何となく気になったので…
Q. include無しでprintfを使えるのか?
やってみた
int main(void){
printf("hello, world\n");
}
使えるらしいが暗黙的な関数宣言として警告が出る
$ gcc main.c
main.c: In function ‘main’:
main.c:2:5: warning: implicit declaration of function ‘printf’ [-Wimplicit-function-declaration]
2 | printf("hello, world\n");
| ^~~~~~
main.c:2:5: warning: incompatible implicit declaration of built-in function ‘printf’
main.c:1:1: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
+++ |+#include <stdio.h>
1 | int main(void){
$ ./a.out
hello, world
しかし、レギュレーション違反な気がします。偶然printfがリンクされていてコンパイラが好意的に解釈してくれたおかげ動いているに過ぎない。環境に大きく依存している。
(追記)@SaitoAtsushi さまから有益なコメントをいただきました。ありがとうございます。
- コンパイルオプションとして -std=c89 をつける
- printf ではなく puts で出力する
とすれば警告は出ません。
暗黙の関数宣言は C89 には仕様としてちゃんと記載がありますが C99 で廃止されたという経緯があるので C89 として解釈するのならば printf の宣言 (stdio.h の include) が無くても正しいプログラムです。 しかし printf は GCC ではビルトイン関数扱いで少し検証が細かいようなので警告も出さないようにするには printf を避ける必要がありました。
ご指摘を受けて確認した結果です。puts かつ c89指定で仕様に準拠した#include無しのHelloWorldが書けますね。警告も出ません。
$ cat main_basic.c
int main(void){
puts("hello, world");
}
$ gcc -std=c89 main_basic.c
$ ./a.out
hello, world
Q. 何もリンクせずにHello Worldを書けるのか?
何もリンクせずに自立したバイナリならば文句がなさそう。
C言語に直接アセンブリを書き、その中でsyscallのwriteを叩くことで実現した。
なお、このソースコードは64bitのLinux環境でしか動かない。
Windowsのシステムコールはラッパーを挟まず直接叩かれることを想定していないようで情報が少なかったので断念した。
int main(void){
const char mes[] = "hello, world\n\0";
asm volatile(
"movq %0, %%rsi \n\t movq %1, %%rdx \n\t movq $1, %%rax \n\t movq $1, %%rdi \n\t syscall"
:
:"r"(mes),"r"(sizeof(mes))
:
);
}
警告すら出ない
$ gcc main.c
$ ./a.out
hello, world
参考
(追記)@fujitanozomu さまから有益なコメントをいただきました。ありがとうございます。
何もリンクしないのであればコンパイル時のコマンドラインオプションに-nostdlibを指定するべきと思います。
Link Options (Using the GNU Compiler Collection (GCC))
https://gcc.gnu.org-nostdlib
Do not use the standard system startup files or libraries when linking. No startup files and only the libraries you specify are passed to the linker, and options specifying linkage of the system libraries, such as -static-libgcc or -shared-libgcc, are ignored.-nostdlibを指定すると記事のコードそのままではリンク時に警告が出、実行すると異常終了する結果となったので対策が必用となります。
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 00000000004000e8 hello, world Segmentation fault
[C] gcc 11.1.0 - Wandbox
https://wandbox.org対策例
main.cvoid _start(void){ const char mes[] = "hello, world\n"; __asm __volatile( "lea %2,%%rsi;syscall" : :"a"(1), "D"(1), "m"(mes), "d"(sizeof(mes) - 1) :"rsi" ); __asm __volatile( "syscall"::"a"(60), "D"(0) ); }
提示していただいたコードを実行すると確かにHello Worldの出力を確認できます。
$ cat main_fixed.c
void _start(void){
const char mes[] = "hello, world\n";
asm volatile(
"lea %2,%%rsi;syscall"
:
:"a"(1), "D"(1), "m"(mes), "d"(sizeof(mes) - 1)
:"rsi"
);
asm volatile(
"syscall"::"a"(60), "D"(0)
);
}
$ gcc -nostdlib main_fixed.c
$ ./a.out
hello, world
変化点は以下だと思っています。
- __startの呼び出しからvoid main()の呼び出しまでにstdlibがスタートアップ処理(コマンドライン引数をレジスタに詰める処理等)を行うが、stdlibがリンクされていないので__startから直接書き始めている
- return 0もstdlibが行っていたのでこれもsyscall 60 exitでrdi=0をOSに返している
- アドレスでは無くメモリをleaで渡して最適化により意図しない動作が生じることを避けている
詳しい話は私の知識量では説明しきれないので記事下の @fujitanozomu さまのコメントを確認してください。
お二人とも私の拙い記事に素晴らしいコメントを付けていただきありがとうございます。