わらばんし仄聞記

南の国で引きこもってるWeb屋さん

原書で学ぶ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プロセスで開けるファイル数の限度に達するのを避けるためです。