はじめに
皆さんはC言語を習うとき、最初に「処理はmain関数から始まる」と習います。しかし、「教科書を疑え」という言葉があるように、「処理はmain関数から始まる」のも疑うべきではないでしょうか。ということで今回はmain関数じゃない関数から処理を始める方法を書きたいと思います。
本記事のテーマはしょうもないですが、それなりに勉強にもなったので本記事を書いてみた次第です。
なお、動作環境はWindows11 WSL上でUbuntu 24.04を使用します。
そもそもなんでmainなのか
A: C言語の規格書に書いてあるから。
例えばC99の規格書のドラフト版ISO/IEC 9899:1999 Committee Draftの「5.1.2.2.1 Program startup」という項目に以下の記述があります。
The function called at program startup is named main.
誰がmainを呼び出すのか
プログラムを実行する場合、基本的にはOSがプログラムをメモリにロードし、処理を開始します。この処理の開始地点がmainということでしょうか。実は違います。適当なプログラム(ここではtestという名前とする)を例に、どこからプログラムの実行が始まるか見てみます。
プログラムのエントリポイント(開始地点)は、ELFヘッダにあります。readelfコマンドで確認してみましょう。
$ readelf -h test | grep Entry
Entry point address: 0x1080
エントリポイントのアドレスは0x1080でした。それでは、0x1080番地に何があるか、objdumpで逆アセンブルして見てみましょう。
$ objdump -d test | grep -C 3 1080
Disassembly of section .text:
0000000000001080 <_start>:
1080: f3 0f 1e fa endbr64
1084: 31 ed xor %ebp,%ebp
1086: 49 89 d1 mov %rdx,%r9
1089: 5e pop %rsi
見てわかるとおり、mainではなく_startという関数が最初に実行されることがわかりました。この_startという関数は、標準Cライブラリが提供するスタートアップルーチンであり、このスタートアップルーチンが最終的にmainを呼び出すことでプログラムが開始します。
main以外の関数から処理を開始する
以上の議論より、main以外の関数から処理を開始する方法として以下の2点を考えます。
- エントリポイントを
_startから別の関数に変更する - スタートアップルーチンを書き換え、
main以外の関数から開始するようにする
これら2つの方法をそれぞれ説明していきます。
エントリポイントを変更する
まずは今回のテスト用プログラムとして、以下test.cを考えます。
#include <stdio.h>
int main1(void)
{
printf("func name: %s\n", __func__);
return 0;
}
main関数の代わりにmain1という関数を定義しました。
エントリポイントを変更するのは実は簡単で、リンカの引数に-e <エントリポイント>を追加すれば良いです。しかし、単に追加するだけだと「main関数がない」とリンクエラーが出てしまいます。これは(使用しないにも関わらず)スタートアップルーチンが暗黙的にリンクされてしまい、mainというシンボルをリンク時に必要としてしまうからです。これを防ぐために-nostdlibオプションも追加します。
以下コマンドによりコンパイルします。
gcc test.c -o test -Wl,-e,main1 -nostdlib -lc
無事コンパイルされました。早速実行します。
$ ./test
func name: main1
Segmentation fault (core dumped)
実行されたものの、セグフォで落ちてしまいました。これはエントリポイントを変更したことにより、スタートアップルーチンを経ずにmain1を呼び出したことによるものです。
通常はスタートアップルーチンがmainを呼び出し、main終了時にスタートアップルーチンがexitを呼ぶことでプロセスを終了させます(以下の図参照)。

しかし今回は、いきなりmain1を呼び出しているため、そこからの戻り先が設定されていません。そのためとんでもない番地にリターンしてしまいセグフォが発生してしまった、ということです(以下の図参照)。

これを防ぐためには、main1の中でexitを明示的に呼び出せばよいです。
#include <stdio.h>
#include <stdlib.h>
int main1(void)
{
printf("func name: %s\n", __func__);
exit(0);
return 0;
}
再度コンパイルし実行してみましょう。
$ ./test2
func name: main1
無事実行できました。
スタートアップルーチンを変更する
エントリポイントを変更する方法では、スタートアップルーチンを実行しないため不具合が起きていました。そのため、スタートアップルーチンそのものを変更し、main以外の関数から開始する方法を検討します。
スタートアップルーチンは標準Cライブラリに含まれます。Linuxでは一般的にGlibcが使用されています。
まずは以下コマンドによりGlibcをダウンロードし展開します。
wget https://ftp.jaist.ac.jp/pub/GNU/glibc/glibc-2.42.tar.xz
tar xf glibc-2.42.tar.xz
cd glibc-2.42
まずは変更する前に普通にビルドしてみます。ビルドする際、ログをファイルに書き出すようにします。スタートアップルーチン単体でコンパイルする際、長ったらしいコンパイルオプションを指定しないとコンパイルが通りません。そのため、コンパイル時のコマンドを把握するためにログをとっておきます。
mkdir build
cd build
../configure --prefix=/dummy --disable-werror
make -j$(nproc) > make.log
さて、本題のスタートアップルーチンの変更です。sysdeps/x86_64/start.Sにmainというシンボルを指定することで、main関数から開始するようにしているようです。ここを変えてみましょう。なお、mainと指定している箇所が2箇所ありますが、コンパイルオプションによりPICが定義されるので、そちらを変更すれば良いです。
#ifdef PIC
mov main@GOTPCREL(%rip), %RDI_LP
#else
mov $main, %RDI_LP
#endif
start.Sは最終的にScrt1.oというファイルにコンパイルされます。Scrt1.oのコンパイルコマンドをログから検索します。
$ grep Scrt1.o make.log -m 1
gcc -nostdlib -nostartfiles -r -o /path/to/glibc-2.42/build/csu/Scrt1.o /path/to/glibc-2.42/build/csu/start.os /path/to/glibc-2.42/build/csu/abi-note.o /path/to/glibc-2.42/build/csu/init.o
どうも複数のファイルを一つのオブジェクトファイルにまとめているようですね。今回変更したstart.Sはstart.osにコンパイルされるようですので、そちらもログからコンパイルコマンドを検索してみましょう。
$ grep /start.os make.log -m 1
gcc ../sysdeps/x86_64/start.S -c -I../include -I/path/to/glibc-2.42/build/csu -I/path/to/glibc-2.42/build -I../sysdeps/unix/sysv/linux/x86_64/64 -I../sysdeps/unix/sysv/linux/x86_64/include -I../sysdeps/unix/sysv/linux/x86_64 -I../sysdeps/unix/sysv/linux/x86/include -I../sysdeps/unix/sysv/linux/x86 -I../sysdeps/x86/nptl -I../sysdeps/unix/sysv/linux/wordsize-64 -I../sysdeps/x86_64/nptl -I../sysdeps/unix/sysv/linux/include -I../sysdeps/unix/sysv/linux -I../sysdeps/nptl -I../sysdeps/pthread -I../sysdeps/gnu -I../sysdeps/unix/inet -I../sysdeps/unix/sysv -I../sysdeps/unix/x86_64 -I../sysdeps/unix -I../sysdeps/posix -I../sysdeps/x86_64/64 -I../sysdeps/x86_64/fpu/multiarch -I../sysdeps/x86_64/fpu -I../sysdeps/x86/fpu -I../sysdeps/x86_64/multiarch -I../sysdeps/x86_64 -I../sysdeps/x86/include -I../sysdeps/x86 -I../sysdeps/ieee754/float128 -I../sysdeps/ieee754/ldbl-96/include -I../sysdeps/ieee754/ldbl-96 -I../sysdeps/ieee754/dbl-64 -I../sysdeps/ieee754/flt-32 -I../sysdeps/wordsize-64 -I../sysdeps/ieee754 -I../sysdeps/generic -I.. -I../libio -I. -D_LIBC_REENTRANT -include /path/to/glibc-2.42/build/libc-modules.h -DMODULE_NAME=libc -include ../include/libc-symbols.h -DPIC -DSHARED -DTOP_NAMESPACE=glibc -DASSEMBLER -I/path/to/glibc-2.42/build/csu/. -fcf-protection -include cet.h -g -Werror=undef -Wa,--noexecstack -o /path/to/glibc-2.42/build/csu/start.os -MD -MP -MF /path/to/glibc-2.42/build/csu/start.os.dt -MT /path/to/glibc-2.42/build/csu/start.os
上記コマンドによりstart.os、Scrt1.oの順でコンパイルすることでmain1から開始するよう設定されたスタートアップルーチンが完成します。
test.cをコンパイルし、今回コンパイルしたスタートアップルーチンをリンクします。
gcc -o test -nostdlib csu/Scrt1.o csu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o ../../test.c -lgcc -lgcc_s -lc /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o csu/crtn.o
以上でmain以外の関数から開始するプログラムができます。
番外編:静的リンクの場合
静的リンクの場合、若干やり方が異なります。静的リンク時start.Sはcrt1.oにコンパイルされます。
$ grep /crt1.o make.log
gcc -nostdlib -nostartfiles -r -o /path/to/glibc-2.42/build/csu/crt1.o /path/to/glibc-2.42/build/csu/start.o /path/to/glibc-2.42/build/csu/abi-note.o /path/to/glibc-2.42/build/csu/init.o /path/to/glibc-2.42/build/csu/static-reloc.o
start.oのコンパイルコマンド検索結果は以下の通りです。
$ grep /start.o make.log -m 1
gcc ../sysdeps/x86_64/start.S -c -I../include -I/path/to/glibc-2.42/build/csu -I/path/to/glibc-2.42/build -I../sysdeps/unix/sysv/linux/x86_64/64 -I../sysdeps/unix/sysv/linux/x86_64/include -I../sysdeps/unix/sysv/linux/x86_64 -I../sysdeps/unix/sysv/linux/x86/include -I../sysdeps/unix/sysv/linux/x86 -I../sysdeps/x86/nptl -I../sysdeps/unix/sysv/linux/wordsize-64 -I../sysdeps/x86_64/nptl -I../sysdeps/unix/sysv/linux/include -I../sysdeps/unix/sysv/linux -I../sysdeps/nptl -I../sysdeps/pthread -I../sysdeps/gnu -I../sysdeps/unix/inet -I../sysdeps/unix/sysv -I../sysdeps/unix/x86_64 -I../sysdeps/unix -I../sysdeps/posix -I../sysdeps/x86_64/64 -I../sysdeps/x86_64/fpu/multiarch -I../sysdeps/x86_64/fpu -I../sysdeps/x86/fpu -I../sysdeps/x86_64/multiarch -I../sysdeps/x86_64 -I../sysdeps/x86/include -I../sysdeps/x86 -I../sysdeps/ieee754/float128 -I../sysdeps/ieee754/ldbl-96/include -I../sysdeps/ieee754/ldbl-96 -I../sysdeps/ieee754/dbl-64 -I../sysdeps/ieee754/flt-32 -I../sysdeps/wordsize-64 -I../sysdeps/ieee754 -I../sysdeps/generic -I.. -I../libio -I. -D_LIBC_REENTRANT -include /path/to/glibc-2.42/build/libc-modules.h -DMODULE_NAME=libc -include ../include/libc-symbols.h -DPIC -DTOP_NAMESPACE=glibc -DASSEMBLER -I/path/to/glibc-2.42/build/csu/. -fcf-protection -include cet.h -g -Werror=undef -Wa,--noexecstack -o /path/to/glibc-2.42/build/csu/start.o -MD -MP -MF /path/to/glibc-2.42/build/csu/start.o.dt -MT /path/to/glibc-2.42/build/csu/start.o
test.cは以下の通りコンパイル、リンクします。
gcc -o test -nostdlib csu/crt1.o csu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginT.o ../../test.c -Wl,--start-group,-lgcc,-lgcc_eh,-lc,--end-group /usr/lib/gcc/x86_64-linux-gnu/13/crtend.o csu/crtn.o -static
おわりに
main関数以外の関数から処理を開始したい、というしょうもない考えでしたが、どのようにmain関数が呼ばれているか、Cライブラリのコードも参照しつつ勉強になりました。このような素朴な疑問から勉強になることは多いので、いろいろと疑ってかかる姿勢を忘れずにいきたいです。
参考
Hello Worldを題材に、main関数の前には何があるのか、printfは何をしているか等の素朴な疑問に答えていく本です。誰もが書いたことのあるHello Worldが「実はこうなっていた」というのがわかる良書です。