結論
できない。
具体的な話
疑問
main関数はプログラムの実行開始点として動作し、ほかのどの関数からも呼び出させず、一般的にはプログラムの終了ステータスを戻します。ということは、mainは宣言されたファイル内で完結した関数に見えます。よって、staticをつけても動作するのではないかと思えました。
ちなみに、Microsoft Learn, "main function and program execution" ではご丁寧にstaticで宣言できないと書いてあります。
Can't be declared as static.
事前知識
static
GNU C Reference Manualの "5.9 Static Functions" の説明を見てみましょう。
You can define a function to be static if you want it to be callable only within the source file where it is defined: (後略)
前述した通り、staticは記述されたファイル内のみで参照できる変数や関数を宣言する修飾子であると書いてあります。
そのファイルでしか使わないような変数や関数につけてあげるとエラーの原因をつぶせる可能性があり、とても便利です。
main関数
今度は GNU C Reference Manualの "5.7 The main Function" を見てみましょう。
Every program requires at least one function, called ‘main’. This is where the program begins executing. You do not need to write a declaration or prototype for main, but you do need to define it. (中略) In general, the return value from main indicates the program’s exit status. A value of zero or EXIT_SUCCESS indicates success and EXIT_FAILURE indicates an error. Otherwise, the significance of the value returned is implementation-defined.
こんなことが書いてあります。
- すべてのプログラムにはmain関数が必要
- プログラムはmain関数を開始点としている
- 一般的には、mainからの戻り値はプログラムの終了ステータス
実行環境
- OS: Ubuntu 24.04.4 LTS
- Compiler: gcc 13.3.0
- Target: x86_64-linux-gnu
実行
以下のようなテストファイルを作成します。このファイルを、プリプロセッサ、コンパイル、アセンブル、リンクの順で行います。
#include <stdio.h>
static int main(void)
{
printf("Hello, World!\n");
return 0;
}
まずプリプロセッサです。以下を実行し、特にエラーは出ずmain.iが出力されました。
$ gcc -E -o main.i
次は、コンパイルです。以下を実行した結果、警告が出ました。
$ gcc -S -Wall -Wextra -Wpedantic main.i -o main.s
main.c:3:12: warning: ‘main’ is normally a non-static function [-Wmain]
3 | static int main(void)
| ^~~~
main.c:3:12: warning: ‘main’ defined but not used [-Wunused-function]
これは、「普通mainにstaticつけないよ」と教えてくれています。また、「mainが定義されたけど使用されていないよ」とも言っています。
ただ、コンパイルが失敗したわけではありません。警告はしてくれますが、main.sは作られています。コンパイルは通りました。
次は、アセンブルです。これは、エラーを出さずmain.oを出力しました。
$ gcc -c main.s -o main.o
最後に、リンクです。実行すると、以下のようなエラーが出てリンクに失敗しました。
$ gcc main.o -o main
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x1b): undefined reference to `main'
collect2: error: ld returned 1 exit status
重要なのは、リンク時のエラー出力であるundefined reference to `main'の部分です。これは、リンク段階でmainが見つからなかったというエラーです。
説明
簡単めな説明
なぜリンカはmain関数を見つけられなかったのでしょうか。それは、staticで宣言したからです。staticは、ファイル内のみで参照できる変数や関数を宣言します。リンクするにはファイルの外側からmain関数を探す必要があるわけですが、staticがそれを邪魔してプログラムの開始位置が見つけられなかったのです。
もう少し詳しく
ここで、リンクの仕組みについて少し取り上げましょう。
シンボルテーブル
リンクを行う際、(staticのつかない)関数やグローバル変数に関して、リンカに名前を結合してもらうために各オブジェクトファイルはシンボルテーブルという表も持っていることが多いです。これは、nmコマンドで見ることができます。
静的な変数や関数に関しても、リンカに何らかのアドレスを割り当ててもらう必要があるため、シンボルテーブルに記載されます。
実際に今回のファイルに行ってみると、以下のようになります。
$ nm main.o
0000000000000000 t main
U puts
mainの前にtとあります。これは、変数や関数がどのように扱われているかのフラグです。
Linux man pageのnmコマンドを見てみると、tはテキストセクションにあるシンボルだとあります。
"t"
The symbol is in the text (code) section.
実はTというフラグもあり、これもテキストセクションにあるシンボルです。この違いは、ローカルかグローバルかの違いになります。
今回、mainはstaticをつけました。結果として、このmainは外部ファイルからの参照を受け付けません。よって、tがついています。通常のmain関数にはTがつきます。
Scrt1.o
さて、リンクでのエラーメッセージにはScrt1.o: in function `_start'とありました。
uClibcのcrt.txtでは、以下のように説明があります。説明の中でcrt1.oとあるので、こちらの説明も載せておきます。
Scrt1.o
Used in place of crt1.o when generating PIEs.
crt0.o crt1.o etc...
This object is expected to contain the _start symbol which takes care of bootstrapping the initial execution of the program. (中略)
On uClibc/glibc systems, this object initializes very early ABI requirements (like the stack or frame pointer), setting up the argc/argv/env values, and then passing pointers to the init/fini/main funcs to the internal libc main which in turn does more general bootstrapping before finally calling the real main function.
まず、Scrt1.oは説明の通り、crt1.oの代わりとしてPIEsが生成されるときに使用されます。
PIEとは、Position Independent Executableの略です。プログラムをロードするアドレスが固定されていない実行ファイルのことです。なぜこんなものがあるのかというと、セキュリティ機能のASLRと相性がよいからです。
そして、crt1.oには、プログラムの初期実行のブートストラップ処理を行う_startシンボルが含まれています。エラーメッセージに書かれていたものです。いくつか処理を行って、実際のmain関数を呼び出すと言っています。
改めて全体の説明
リンカが通らない理由は、プログラムの初期実行のブートストラップ処理の段階で、プログラムの実行点であるはずのmainが見当たらないからです。一方、コンパイラが通った理由は、文法や型、関数定義としては正しいからです。「ファイル内だけで使えるmainという関数を定義した」という風になっています。この時点では、リンクの段階でスタートアップオブジェクトから参照可能かは問題にならなかったわけです。