連載
» 2017年07月13日 05時00分 UPDATE

main()関数の前には何があるのか(9):エンジニアならC言語プログラムの終わりに呼び出されるexit()の中身分かってますよね? (2/3)

[坂井弘亮,著]

_exit()のソースコードを読む

 glibcの_exit()のソースコードを読んでみよう。

 そのためには、まずは_exit()が定義されている場所を探さなければならない。しかし単純に「exit」や「_exit」で検索しても、大量にヒットしてしまいそうだ。どのようにして探したらいいだろうか。

 このようなとき、まずは探す対象がアーキテクチャ共通の処理なのか、そうではなく特定のアーキテクチャ特有の処理なのかをまず考えるといい。図5.9を見るとhltという命令が呼ばれているが、これはx86特有の命令でコンパイラが出力するようなものではない。ということは、x86依存のディレクトリに_exitの実体があるのではないだろうか。しかもおそらく、アセンブラのファイルだろう。

 システムコール・ラッパーを探したとき、glibcではx86特有の処理はsysdeps/unix/sysv/linux/i386というディレクトリにあった。そこでhlt命令を利用している箇所を探してみよう。

[user@localhost ~]$ cd glibc-2.21/sysdeps/unix/sysv/linux/i386
[user@localhost i386]$ grep hlt *
_exit.S:	hlt
makecontext.S:	hlt
[user@localhost	i386]$

 _exit.Sというファイルがある。内容を見てみると、以下のようになっていた。

21:	.type	_exit,@function
22:	.global	_exit
23:_exit:
24:	movl	4(%esp), %ebx
25:
26:	/* Try the new syscall first. */
27:#ifdef __NR_exit_group
28:	movl	$__NR_exit_group, %eax
29:	ENTER_KERNEL
30:#endif
31:
32:	/* Not available. Now the old one. */
33:	movl	$__NR_exit, %eax
34:	/* Don't bother using ENTER_KERNEL here. If the exit_group
35:	   syscall is not available AT_SYSINFO isn't either. */
36:	int	$0x80
37:
38:	/* This must not fail. Be sure we don't return. */
39:	hlt

 内容はまさに図5.9と一致するので、これが_exitの定義のようだ。システムコールを呼び出しているので、どうやらシステムコール・ラッパーのようだ。

 exit_groupの呼び出し処理は#ifdef __NR_exit_groupでくくられているので、exit_groupが存在する場合にはそれが呼ばれるが、そうでない場合にはexitが呼ばれる、ということらしい。コメントにも「Try the new syscall first.」や「Not available. Now the old one.」のように書かれており、まず新しいシステムコール(exit_group)を試すが、ダメなら古いほう(exit)を使う、というように書かれている。

 システムコール番号から考えても、exitの1に対してexit_groupは252であり、exitは初期から存在するシステムコールだが、exit_groupはおそらくだいぶ後に追加されたシステムコールだと推測できる。またglibcの_exitは、Linuxカーネルにexit_groupが追加された際にその呼び出しが#ifdefでくくることで追加された、ということも推測できる。

 exit_groupの機能を考えると、スレッド機能が追加されたときに追加されたシステムコールなのだろうか。

exit()と_exit()とexit_groupとexit

 exit()や_exit()、そしてexit_groupとexitなどいろいろ出てきているが、ここで一度整理しよう。

  • exit()…………… プログラムの終了時に呼び出されるライブラリ関数(glibc)
  • _exit() ………… exit()の中から呼ばれ、exit_groupを呼び出すシステムコール・ラッパー(glibc)
  • exit_group …… プログラムを終了させるLinuxのシステムコール(Linux)
  • exit……………… プログラムを終了させるLinuxの旧来のシステムコールだが、現在はexit_groupが主に使われる(Linux)

 そしてPOSIXでは、プログラムを終了させるAPIとして、以下の2つが定義されている。なおatexit()はexit()による終了時に呼び出してほしい関数を登録しておくことができる、ライブラリ関数だ。

  • exit()……………… atexit()によって登録された関数を呼び出してから終了する
  • _exit() …………… atexit()によって登録された関数を呼ばずに終了する

 これらをどのように考えたらいいだろうか。

 Linuxカーネルが定義するのはシステムコールのABIであり、APIを提供するのはglibcの役割だ。よってPOSIXのexit()と_exit()を実現するための機能としてLinuxにはexit_groupやexitシステムコールがあり、glibcはexit_groupシステムコールを呼び出すことで、POSIXの_exit()を提供している、と考えられるだろう。_exit()はシステムコール・ラッパーとしてユーザに提供されるAPIなわけだ。

 なお、「exit」と表記した場合にはLinuxカーネルが持つexitシステムコールを指し、「exit()」と表記した場合にはglibcが提供する(POSIXの)exit()ライブラリ関数を指している、と考えるべきだろう。括弧の有無によって指すものがまったく異なるものになることに注意してほしい(またglibcの_exitのように、アセンブラ上で定義してあるシンボルを「_exit」のようにして括弧無しで表記する場合もある)。

manのカテゴリを見てみる

 CentOSでexit()と_exit()をmanコマンドで調べたときの、カテゴリはどうなっているだろうか。

 ここで、manコマンドでのカテゴリ指定についてちょっと説明しておこう。

 まずUNIXライクなOSでのmanコマンドによるオンラインマニュアルはカテゴリ分けされており、一般にカテゴリ1がコマンド、カテゴリ2がシステムコールのAPI、カテゴリ3がライブラリ関数に相当する。

 exit()はatexit()によって登録された関数を呼び出してからプログラムを終了する、ライブラリ関数だ。ということはカテゴリ3にあるはずだ。そしてこれをexit(3)のように表記する。

 しかし本書のVM環境でman exitを実行してみると、以下のように表示される。

BASH_BUILTINS(1)					BASH_BUILTINS(1)
 
NAME
	bash, :, ., [, alias, bg, bind, break, builtin, caller, cd, command,
	compgen, complete, compopt, continue, declare, dirs, disown, echo,
	enable, eval, exec, exit, export, false, fc, fg, getopts, hash, help,
	history, jobs, kill, let, local, logout, mapfile, popd, printf, pushd,
	pwd, read, readonly, return, set, shift, shopt, source, suspend, test,
	times, trap, true, type, typeset, ulimit, umask, unalias, unset, wait -
	bash built-in commands, see bash(1)
...

 「BASH_BUILTINS(1)」となっており、シェルのビルトイン・コマンドであるexitがヒットしているようだ。このようにmanコマンドには、同じキーワードが別カテゴリに複数登録されていることがある。

 そのような場合には、man 3 exitのようにしてカテゴリ指定で参照することができる。「exit(3)を調べるように」と言われたら、それはカテゴリ3のexit、つまりman 3 exitを見てみなさいという意味だ。以下はman 3 exitを実行したときの表示結果だ。

EXIT(3)			Linux Programmer's Manual		EXIT(3)
 
NAME
	exit - cause normal process termination
 
SYNOPSIS
	#include <stdlib.h>
 
	void exit(int status);
 
DESCRIPTION
	The exit() function causes normal process termination and the value of
	status & 0377 is returned to the parent (see wait(2)).
...

 先頭で「EXIT(3)」のように表示されているので、ライブラリ関数のexit(3)がヒットしている。ということは、exit()はやはりライブラリ関数の扱いだ。

 _exit()はどうであろうか。man _exitを実行してみよう。

_EXIT(2)		Linux Programmer's Manual		_EXIT(2)
 
NAME
	_exit, _Exit - terminate the calling process
 
SYNOPSIS
	#include <unistd.h>
 
	void _exit(int status);
...
DESCRIPTION
	The function _exit() terminates the calling process "immediately". Any
	open file descriptors belonging to the process are closed; any children
	of the process are inherited by process 1, init, and the process's par-
	ent is sent a SIGCHLD signal.
...

 こちらは先頭で「_EXIT(2)」と表示されている。つまり_exit(2)なので、_exit()はシステムコールAPIの扱いになっていることになる。

Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。