原書で学ぶ64bitアセンブラ入門(8)
http://connpass.com/event/6463/原書で学ぶ64bitアセンブラ入門(6) - connpass での内容です。 この会も6回を数え、全19章の内14章まで終わってだいぶ佳境に入ってきた感じです。
今回は12章から14章までを読んでいきましたので、まずは12章をば。
chapter12 System calls
12章はシステムコールについての話です。
システムコールとは、大雑把に言ってしまえばカーネルのみが実行できる関数といった感じで、通常のユーザーモードのプログラムが好きなように実行出来る様にするには危険であり、そのような命令をカーネルに任せる為のものです。
システムコールが発生すると、CPUのオペレーションモードがユーザーモードからカーネルモードへ変わり、諸々の処理が実行できるようになります。尚、Linuxのシステムコールインターフェースは32ビットモードと64ビットモードで異なります。
32bit system calls
32bitシステムコールの定義は/usr/include/asm/unistd_32.h
で定義されています。このファイル内で定義されているシステムコール番号をeaxに入れ、ソフトウェア割り込み命令を実行するint 0x80
事でシステムコールを実行します。システムコールへのパラメーターはebx
ecs
edx
esi
edi
ebp
のレジスタを使って渡され、返値はeax
に設定されます。
64bit system call
64bitシステムコールの定義は/usr/include/asm/unistd_64.h
で定義されています。パラメーターはrdi
rsi
rdx
r10
r8
r9
を通じて渡され、返値はrax
に渡されます。これはCの関数コールで使われたレジスタと、r10
の箇所がrcx
であった事を除いて変わりありません。また、32bitではint 0x80
を使っていた代わりにsyscall命令を使います。例として、64bit版の"Hello world"はこんな感じになります。
C wrapper functions
いずれのシステムコールもCのラッパー関数を通じて使うことができます。例としてCライブラリのwrite関数は、書き込みリクエストをするsyscall命令を使う以外はほとんど何もしません。このラッパー関数を使った方がシステムコール番号を探したりする必要が無いため、システムコールの使い方としては好まれている様です。
先ほどの"Hello world"プログラムは下記のように書き換えることが出来ます。
ここではexternを使ってwriteとexitが他の場所で定義されていることをリンカに伝えています。
open system call
ファイルの読み書きには、その対象がオープンされている必要があります。これはopenシステムコールによって為されます。
int open ( char *pathname, int flags [, int mode] );
pathname
には開く対象のファイル名、flags
にはファイルがどのように開かれるかを定義したビットパターン、もしflags
でファイルが作られるパターンである場合はmode
で新規作成されるファイルの権限割り当てを定義します。flags
の定義は以下の通り
ビット | 意味 |
---|---|
0x000 | 読み込み専用 |
0x001 | 書き込み専用 |
0x002 | 読み書き |
0x040 | 必要なら新規作成 |
0x200 | ファイルのtruncate |
0x400 | 追記 |
基本的な権限設定は読み書き実行の指定です。ファイルへの実行権限付与はプログラムやスクリプトを実際に実行することが出来るということですが、ディレクトリへの実行権限の有無はそのディレクトリをたどることが出来るかどうかに関係します。
例として、./tmp/testfile
というディレクトリとファイルがあった場合について。
$ ls -al tmp/
total 8
drwxr-xr-x 2 warabanshi users 4096 Jun 15 09:41 .
drwxr-xr-x 3 warabanshi users 4096 Jun 15 09:41 ..
-rw-r--r-- 1 warabanshi users 0 Jun 15 09:41 testfile
通常、作成したばかりならこの様にディレクトリ内を閲覧する事が出来る。ここで、tmpディレクトリの実行権限を消してみると
$ chmod 655 tmp/
$ ls -al tmp
ls: cannot access tmp/..: Permission denied
ls: cannot access tmp/testfile: Permission denied
ls: cannot access tmp/.: Permission denied
total 0
d????????? ? ? ? ? ? .
d????????? ? ? ? ? ? ..
-????????? ? ? ? ? ? testfile
この様になり、tmp/ディレクトリには読み込み権限はある為、このtmp/ディレクトリエントリにある内容(配下にあるファイル名など)は閲覧出来るがその詳細については取得できないため、?マークが並んでいる。
権限については「所有者」「グループ」「その他」に対して権限を付与できるわけですが、この辺りは今更ですしいくらでもドキュメントが転がってるので割愛。
read and write system calls
ファイルへのデータ読み書きはreadとwriteシステムコールで行われ、プロトタイプは下記のようになっています
int read ( int fd, void *data, long count );
int write ( int fd, void *data, long count );
dataはいかなる型でも問題ありませんが、countには読み書きするバイト数を指定します。エラー時は返値が-1となり、extern変数のerrnoにエラータイプを示す整数がセットされる事で示されます。また、エラーのテキスト版をperror関数の呼び出しで出力することが出来ます。
lseek system call
ファイルの読み書きをする際、特定の位置だけにアクセスできれば良いならば、取り得る方法は2つあります。ファイル全体を読み込んで対象の箇所を操作するか、lseekを使って対象箇所の始点へ移動して必要な分だけ操作するかです。無論、後者の方が速いです。lseekのプロトタイプは
long lseek ( int fd, long offset, int whence );
となっています。offsetでどの位置に移動するかを指定できますが、その基準はwhenceの値によって変わります。
whence | 基準 |
---|---|
0 | 対象fdの最初の位置からのバイト数 |
1 | 現在の位置からの相対位置 |
2 | ファイル末尾からの相対位置 |
offsetを0、whenceを2とすると、これによりファイルサイズを容易に取得することが出来ます。
close system call
ファイルをopenした後、そのまま放っておいてもプログラム終了時にはOSによって閉じられるので、そういう意味では絶対に何がなんでもcloseしなければならないということはありません。ファイルディスクリプタを使ってのデータ読み書きはバッファも使っていないので、closeを明示的にしないとデータが失われるということもありません。
closeする意義は、カーネルのオーバーヘッドを減らすことと、実行している1プロセスで開けるファイル数の限度に達するのを避けるためです。