OSvのCLI周辺コードをみてみる
※この記事は OSv Advent Calendar 2014 9日目の記事です
最近、ちょこちょことOSvを触らせてもらってるので、OS関連周りにはあまり明るくないながらも書かせてもらう事にしました。 こっち方面を専門としている訳ではない為、間違っている箇所などありましたらご指摘ください。
さて、CLI周りについてなのですが、前回のOSvもくもく会#4の時にこの辺りの構造について見ていたのでそこら辺触れてみようかと。adventcalendar始まってみたら @syuu1228 さんとかぶり気味だったので変えようかと思ったけど、そんなにホイホイとネタが出てくるわけでもないので突き進みます。
OSvのCLIについて
元々、OSvがjavaアプリを動かすという想定で始まっている事もあり、しばらくはCLIのシェルとしてCRaSHってのを使ってました。それをリプレースする物として、Luaで書かれたCLIが作られたそうです。後者については既に三日目のcalendarで軽く触れられてますね。
このCLI周りについて、チラッと見てみた事を徒然と書いていきます。 そうそう、OSvは凄い勢いで機能追加やらされてるので、今回書いている事もすぐに変わってしまうかもしれません。この記事は v0.16-12-gbc3b9f6 を元に書いています。
CLIのコードを見てみる
最初の一歩
場所については先ほど触れた三日目の記事で紹介されている通り、こちらにあります。pathを見ての通り、このCLIもmoduleの1つとして実装されていますね。ディレクトリ内はこんな感じ
$ ls -1
cli.c
cli.lua
cli.so
commands
doc
lib
Makefile
module.py
rpmbuild
test
module.pyはこのmoduleの起動処理辺りが書いてある様で、内容は下記の通り
from osv.modules.api import * from osv.modules.filemap import FileMap from osv.modules import api require('lua') require('ncurses') require('libedit') require_running('httpserver') usr_files = FileMap() usr_files.add('${OSV_BASE}/modules/cli').to('/cli') \ .include('cli.so') \ .include('cli.lua') \ .include('lib/**') \ .include('commands/**') full = api.run('/cli/cli.so') default = full
cli.soをrunしてますね。この段階で、cli.cがcliコマンド(これも例の三日目の記事で使ってましたね)の実装となり、これがcli.luaに処理を渡してcommandディレクトリ配下に記述のあるluaで書かれたコマンドを実行してるのかなーって予想が立ちますね。
cli.c
そんでは、cli.cを見てみましょうか。全部トレースしていくとこの記事だけでは終わらなそうなので、要所だけ。 main()内では大筋として、テストコマンド、単一コマンド実行、シェルの起動に分かれている様です。
int main (int argc, char* argv[]) { ... if (test_command != NULL) { ... lua_getglobal(L, "cli_command_test"); ... else if (optind < argc) { /* If we have more arguments, the user is running a single command */ ... lua_getglobal(L, "cli_command_single"); ... } else { /* Start a shell */ ... lua_getglobal(L, "cli_command"); ... } ... }
こんな感じで各々のパターンに応じてluaでglobalとして作られているメソッドを読んでいる様です。
cli.lua
さて、簡単な所から見てみる事にして、cli_command_singleから見てみましょう。
function cli_command_single(args, optind) local t = {} for i = optind, #args do table.insert(t, args[i]) end cli_command(t) end
用はアレですね。例えば
$ cli -a http://192.168.122.72:8000 ls -lh
とかって記述した場合、ls -lh
の部分だけ抜き取ってcli_commandに投げる感じですかね。
cli_command_testは紙面の都合で飛ばさせてもらい、cli_commandに進んでみます。要所をピックアップすると
function cli_command(args) ... if #arguments > 0 then command = arguments[1] ... filename = command_filename(command) if file_exists(filename) then local cmd = dofile(filename) local cmd_run = true ... if cmd_run then local status, err = pcall(function() cmd.main(arguments) end) if not status then print_lua_error(command, err) end end else print_cmd_err(command, "command not found") end end end
ざっと読むと、ここに渡された引数の最初の文字列と一致するファイルを探し、そのファイルに実装されているものがコマンドの実態として扱われ、実行されるということですね。ちなみにcommand_filename()はlib/util.lua内にあり、
function command_filename(name) return string.format('%s/%s.lua', context.commands_path, name) end
となっています。commands_pathは巡り巡ってcli.c内にて #define CLI_COMMANDS_PATH "/cli/commands"
として定義されていますね。以上より、CLI実行時、各コマンドの処理は/cli/commands内の当該luaファイルが実行されている事まで追えました。ls
コマンドなら/cli/commands/ls.lua
ですね。
コマンドの実装
ということで、コマンドを実装してやるには先述のディレクトリへluaプログラムを置いてやればいいんだろうという判断に至ったわけですが、これ、散々述べている様にLuaで書かれてるんですよ・・・。なんか足りないコマンドを実装してやろうとは思ったんですが、Luaはneovimで使うぞー!って聞いて本買ってみた程度なので、実装能力はほぼ絶無な為、今のところ断念中。
これはOSv全体にわたって言える事なんですが、使ってる言語多すぎでは・・・。ぱっと見た限りでもC++11、python、go、luaが飛び交っております。でもLuaはちょっと興味あるので、これを機に弄ってみるとおもしろいんじゃないかとは目論んでます。
というわけで
何はともあれ、現在private betaが行われているOSv。まだまだとりつく島もないような大規模ではないので、興味あったら読んでみる(できれば何か実装してみたりする)と面白いんじゃないかと思います。パッチの送り方等については四日目の記事を参照ください。
補数表現について
補数って?
2進数を扱うとき、符号付きの場合を考えると決まって出てくる「2の補数表現」ってやつ。 これ自体は「補数」と一般的に言われるものの定義から明確なので、何も問題は無いんです。
wikipedia先生でも述べられている通り、補数とは\( {b} \)進法において、自然数\( {a} \)を表現するのに必要な最小限の桁数を\( {n} \)としたとき
\begin{align*} {b}^{n} - {a} \end{align*}
である。つまり「補数」とは、表したい数について「必要な桁数 +1」桁の最小の数から、その表したい数を引いた値。言い換えれば、表したい数に対して和をとれば桁が上がる値のなかで最小のものということになる。
この辺りについては 原書で学ぶ64bitアセンブラ入門(2) - わらばんし仄聞記 でも触れましたが、例えば10進数で考え、61という値の補数は39になります。
以上より、2進数においては各ビットを反転させたものに1を足したものが「2の補数」であることは容易に理解出来ます。
1の補数表現
さて、悩みのタネであった「1の補数表現」です。
先述の定義にそのまま当てはめて考えてしまうと、「1進法」?なんだそれは・・・。と、完全にポルナレフ状態になってしまいます。
さて、またもwikipedia先生の出番です。
符号付数値表現 - Wikipedia
こちらで書いてある通り、英語の時点では「1の補数」とは "ones' complement" であり、「2の補数」は "two's complement" と表されています。2の補数についてはいいでしょう。英語の方でも2を基数として補完する値ということが読み取れます。
問題の「1の補数」ですが、 "ones'" となっています。-sで終わる複数形の名詞について所有格を示す為には、末尾にアポストロフィだけを付記する事からわかる通り、これは、複数の "1" の並びを補う値という意味が読み取れます。
これはつまり、最初にリンクを貼ったwikipediaのページにある「減基数の補数」を2進数の場合において表している訳で、実際その辺りの書き分けが英語では出来ますよということも述べられていますね。
減基数の補数は英語で "diminished radix complement" というそうで、そこら辺については以下を参照してもらうといいかと。
Method of complements - Wikipedia, the free encyclopedia
つまり
補数をとる対象Nについて、適当な値aについて考えると
N's complement -> Nの冪乗と相補関係(aに対して、「a以上であり、かつ最小のNの冪乗」となるために足すべき数)
Ns' complement -> Nを複数並べたものと相補関係(aに対して、全て桁がNで埋まるように足すべき数)
という違いが存在する。
一般的に「2の補数」と合わせて述べられる「1の補数」は「2進法における減基数の補数」の事であり、これはつまりがビットを反転させたものと一致する。昔は負数の表現に、先頭のビットを1として、残りのビットを対応する正の数を反転させたものを使っていたそうです。その辺りはwikipediaの方が詳しいのでそちらを見てもらったほうがいいですが、この場合、0と-0という0の表現が2つ出来てしまう事になる為に計算が複雑になるため、現在、負の値については+1して-0の位置に-1が来るようにしたものを使います。
さて、2の補数が各ビットを反転させたものに1を足したものとして表現できることは先に述べたとおり。また、直前で述べた負数の表現は「1の補数を求めた(=ビットを反転させた)ものに1を足す」事によって-0を省いた負数表現をしていました。つまり、やっている事は同じ操作ですね。
これより、「負の値を求める時は2の補数」を求める。また、その導出については「全部のビットを反転させて1を足す」という操作の全てがリンクしたことになります。
ほとんどwikipedia読んで思ったことを書いたばかりな感じですが、自分の中では全部つながってスッキリした感じがします。
なにか間違っている点があったら、ご指摘お待ちしてます。
池袋物理学勉強会
池袋物理学勉強会(6) - connpass へ行ってきました。
唐突なのでさわりを述べておくと、以下の本を進めていき、後々は量子力学がわかるようになろうというのを目指している勉強会です。
Amazon.co.jp: 量子力学を学ぶための解析力学入門 増補第2版 (KS物理専門書): 高橋 康: 本
さて、本日は第二章の演習問題を解く回だったのですが、その内の1問を担当したので折角だから忘れないうちに書いておきます。
P.50 演習問題2 問1
設問
空間の与えられた2点を結ぶ線のうち、直線が最短であることを変分法を用いて示せ
解答
題意より、下のような図を考えます。
与えられる2点をa,bとし、その間を結ぶ線分\( l \)(図の青い線分)が最短となる時の式が直線を示す様になっていれば良い。
線分\( l \)上の微小区間\( {ds} \)を考える。\( {ds} \)は
\begin{align*} {ds} = \sqrt{ {\left({dx}\right)}^2 + {\left({dy}\right)}^2 } \end{align*}
である。線分\( l \)は
\begin{align*} l = \int {ds} \end{align*}
と表せるので、先ほどのより
\begin{eqnarray} l=\int \sqrt{ {\left({dx}\right)}^2 + {\left({dy}\right)}^2 } \\= \int {dx} \sqrt{ 1 + {\left(\frac{dy}{dx}\right)}^2 } \end{eqnarray}
で表すことが出来る。\( l \)は汎関数であり、これが停留値をとればよく、つまり変分\( \delta{l} \)が
\begin{align*} \delta{l} = 0 \end{align*}
となればよい。\( {\frac{dy}{dx}} = {y'} \)と置くと
\begin{align*} l=\int {dx} \sqrt{ 1 + {y'}^2 } \end{align*}
であるから
\begin{eqnarray} \frac{\delta{l}}{\delta{y'}}=\int {dx}(1+{y'}^2)^{-\frac{1}{2}} \cdot {y'}\\ \delta{l} = \int {dx} \cdot {y'}(1+{y'}^2)^{-\frac{1}{2}} \cdot {\delta{y'}} \end{eqnarray}
\( {y'} \)を\( {\frac{dy}{dx}} \)に戻して
\begin{eqnarray} \delta{l} = \int {dx} \frac{dy}{dx} {\left\{1 + {\left(\frac{dy}{dx}\right)}^2\right\}}^{-\frac{1}{2}} \frac{d\delta{y}}{dx}\\ \frac{\delta{l}}{\delta{y}} = \int {dx} \frac{dy}{dx} {\left\{1 + {\left(\frac{dy}{dx}\right)}^2\right\}}^{-\frac{1}{2}} \cdot \frac{d}{dx} = 0 \end{eqnarray}
となればよいので、つまり \begin{align*} \frac{d}{dx} {\left[\frac{dy}{dx} {\left\{1 + {\left(\frac{dy}{dx}\right)}^2\right\}}^{-\frac{1}{2}}\right]} = 0 \end{align*}
であればよい。これは、\( {C} \)を定数として
\begin{align*} \frac{dy}{dx} {\left\{1 + {\left( \frac{dy}{dx} \right)}^2 \right\}}^{-\frac{1}{2}} = {C} \end{align*}
の時に成り立つ。よって
\begin{eqnarray} \frac{dy}{dx} = {C}{\left\{1 + {\left( \frac{dy}{dx} \right)}^2 \right\}}^{\frac{1}{2}}\\ {\left(\frac{dy}{dx}\right)}^2 = {C}^2{\left\{1 + {\left( \frac{dy}{dx} \right)}^2 \right\}} \end{eqnarray}
\( {\left(\frac{dy}{dx}\right)}^2 = \alpha \)とすると
\begin{eqnarray} \alpha = {C}^2(1 + \alpha)\\ (1-{C}^2)\alpha = {C}^2\\ \alpha = \frac{{C}^2}{1-{C}^2} \end{eqnarray}
つまり
\begin{align*} \frac{dy}{dx} = {\left(\frac{{C}^2}{1-{C}^2}\right)}^{\frac{1}{2}} \end{align*}
であり、\( {\left(\frac{dy}{dx}\right)}^2 \)は定数であることがわかる。これより\( {l} \)は
\begin{align*} {l} = \int {dx}\sqrt{1 + {C}} \end{align*}
で表せ、\( \sqrt{1 + {C}} \)をまた定数\( {a} \)と置くと
\begin{eqnarray} {l} = \int {a} {dx}\\ = {ax} + {b} \end{eqnarray}
つまり、\( {l} \)は直線である。
余談
途中から\( {y'} \)の二次方程式とおいて、もっとシンプルに解答まで到れる方法もありますが、そこら辺は気が向いたら追記します。
また、問題が変分法を用いて解けということで上記のようにしていますが、Euler-Lagrangeの式を使ってもあっさりできます。
とりあえず、数式は書くのが大変だということがよくわかりました。
原書で学ぶ64bitアセンブラ入門(10)
14章、入出力ストリームについてです。
Using the C stream I/O functions
システムコールの章では、入出力に関してopen
read
write
close
というシステムコールのラッパー関数について見てきました。この章ではバッファ付きI/Oを使う関数について見ていきます。
バッファ付きI/Oを使った方が読み込みが効率的になり、その仕組みは次のようになっています。例として1byteの読み込みを実行すると仮定します。
- 1byteをバッファから読み込もうとする
- バッファに対象のデータが無い
- バッファを満たすだけのデータ(一般的に8192byte)をバッファに読み込む
- バッファに読み込まれたデータから目的の1byteを取得する
この際、バッファからの読み込みは高速な為、ある程度のbyte数を読み込む必要がある場合は最初以外バッファとのやり取りになるので高速に行えます。つまり、8192byte以下の容量ならば実際に呼んでいるシステムコールは1回だけで満足させることが出来ます。
Opening a file
ストリームI/O関数を使ってファイルを開くには、fopen
を使います。fopen
のプロトタイプは
FILE *fopen ( char *pathname, char *mode );
となります。まぁ、よく見るfopen
そのままですね。第1引数がファイル名で第2引数がモードです。モードについても今更ですが、折角本にも書いてあるので一応記しておきます。
mode | 内容 |
---|---|
r | 読み込みのみ |
r+ | 読み書き |
w | 書き込みのみ。ファイルは初期化されるか新規作成する |
w+ | 読み書き。ファイルは初期化されるか新規作成する |
a | 書き込みのみ。ファイルに追記するか新規作成する |
a+ | 読み書き。ファイルに追記するか新規作成する |
fopen
の返値はFILEオブジェクトのポインタです。システムコールのopen
では返値がファイルディスクリプタの番号だったので、ここら辺が異なりますね。まぁ、第2、第3引数も違いますが・・・。
FILEオブジェクトについての詳細には本書では触れていません。そこまで知る必要が無いということで、大抵の場合、FILEオブジェクトはファイルといくつかのファイルについての"状態監視"データ要素へのバッファのポインタを内包した構造体であるとだけ触れています。
返値はポインタということで、アセンブラではquad-wordサイズの領域を確保しておけばそこに保存し、後で使うことが出来ます。ということで、実際に使ってみるとこの様になります。
fscanf and fprintf
fscanf
fprintf
は第1引数にFILEオブジェクトへのポインタを指定出来るようになっています。scanf
printf
は第1引数をそれぞれstdin
stdout
に固定してfscanf
fprntf
を呼び出すといった程度の違いです。
fgetc and fputc
fgetc
fputc
のプロトタイプは
int fgetc ( FILE *fp );
int fputc ( int c, FILE *fp );
fgetc
の返値は基本的に読み込まれた文字。fputc
の返値は基本的に書き込んだものと同じ文字です。
さて、fgetcで1文字を取得した後、余分に取りすぎた場合を考慮し、その1文字をストリームへ戻すための関数ungetc
があります。プロトタイプは
int ungetc ( int c, FILE *fp );
ungetc
で戻せるのは1文字だけで、繰り返しungetc
を実行したからといって際限なくストリームへ戻せるというわけではありません。
この館数の使いどころとして真っ先に思い浮かぶのが構文解析を行うケースで、例えば12345,23456
という,
区切りの2数値について考えてみると、12345
までを1文字ずつ取得して数値として解析していき、,
をfgetc
してストリームから得た段階で数値が終わっていた事が判明します。この1字先読みして取得してある,
を一旦ストリームへ返す事により、数値である範囲を解析した後、それまで行ってきた解析のループ上で,
を解析して必要な処理を行う等の対応が可能になります。これが無いと処理が一般化しづらくて大変ですね。
fgets and fputs
fgets
fputs
。この辺りもよく見かけるモノなので、特別な説明はここでする必要も無いでしょう。一応、プロトタイプは
char *fgets ( char *s, int size, FILE *fp );
int fputs ( char *s, FILE *fp );
本書ではそれぞれの実行時に終端文字の扱いがどうなるかがあれこれ書いてあるので、それらをまとめると
fgets
- 改行を読み込んだなら、バッファも改行が保存される
- 常に読み込んだデータの最後に0x00を置く
fputs
- 改行や終端に0x00を加えるといった操作は使用者の責務
fread and fwrite
fread
fwrite
関数はデータの配列を読み書きするように設計されています。プロトタイプは
int fread ( void *p, int size, int nelts, FILE *fp );
int fwrite (void *p, int size, int nelts, FILE *fp );
第1引数は配列。型は問わず。第2引数は配列要素のサイズで、第3引数は配列の要素数。nelts
はおそらくnumber of elementsの略?最後は言わずもがな、FILEオブジェクトへのポインタ。
customers配列の100要素をファイルへ書き込むにはこんな感じ
mov rdi, [customers]
mov esi, Customer_size
mov edx, 100
mov rcx, [fp]
call fwrite
fseek and ftell
本書では関数の引数説明程度のことしか書かれてないので、適当にman
でも見てください。
fclose
ストリームを閉じる関数です。ストリームはまだデバイス上へ書き込まれていないデータをバッファに持っているかもしれないので、ちゃんとfcloseを呼び出して書き込ませましょう。場合によってはまだ書き込まれていなかったデータが消失する事になります。
原書で学ぶ64bitアセンブラ入門(9)
続いて13章、Structについてです。
今更ですけど、今回この本で読むまでアセンブラに構造体があるなんて知らなかったです。
Struct
例えばCの構造体で
といった、Customerという名の構造体について考えます。
こんな構造体をアセンブラ上でも実現するとして、結局はアドレス、もしくは先頭位置からのoffsetがわかればそれぞれのフィールドに対応する領域はわかるので、こんな風にして各要素へデータを入れるのと同様の事が出来ます。
まぁ、nameやaddressやらのデータについてはこのコード中には定義されてないですが、どっかで適宜定義されていると思っておいてくださいな。
挙動としては1~2行目でCustomer構造体のサイズに相当する136byteをメモリ割り当てしてます。3行目で一旦cにそのアドレスを保存。4行目で構造体の最初のフィールドであるint id
に即値で7を入れてます。5,6行目はname[64]の領域にどこぞで定義されているハズのnameを7行目のstrcpyを使ってコピーするための準備。9~11行目も似たようなものですね。8行目や12行目は構造体先頭からのオフセットで各フィールドを示せるよう、都度raxをセットし直してます。
Symbolic names for offsets
先の例だと位置を数値で直指定なので、当然ながら構造がちょっと変わったらコードの関係するところを全修正の憂き目にあいますね。
ということで、構造体を示すキーワードを使って、ちゃんと構造体を定義してみると
struc Customer
id resd 1
name resb 64
address resb 64
balance resd 1
endstruc
こんな感じになります。構造体の構成はstruc
からendstruc
までの間で示されます。
この書式で定義しておくと、各フィールド名を個別にequ命令でoffset値として定義しておくのと同等の効果が得られます。但し、このままだと各フィールド名がグローバルでの扱いになってしまうので、それの回避としてこんな感じに。
struc Customer
.id resd 1
.name resb 64
.address resb 64
.balance resd 1
endstruc
こうしてprefixとしてドットを付けておくと、参照するにはCustomer.name
みたいに指定しなければなりません。ただ、ちょっと名前が長くなるので面倒ですね。
他に構造体を定義しておくとうれしい事として、(構造体名)_size
として、この構造体の占めるバイト数を取得できるということがあります。今回の場合なら、Customer_size
ですね。これらを使ってさっきのコードを書き直すと
こんな風になります。mallocする領域にCustomer_size
を使ったり、raxからのオフセット指定にフィールド名を使ったりしてますね。これならフィールドの大きさが後で変わったりしても大丈夫。
と、思いきや、これだけで大丈夫なのは今回の場合は運が良かったというか、たまたまそうなってるだけです。何が問題かというと、C言語の構造体では要素のサイズによって、開始位置がアラインメントされてなければならないというのがあります。例えば、今回double-wordサイズのbalanceが始まる位置は、Customerの先頭から数えて132byteの位置からです。これは4の倍数なのでdouble-wordのbalanceに対してはアラインメント不要です。
さて、ではaddressが1byte長くなって65byteになったなら?
C言語上ではbalanceの開始位置をずらし、136byte目からbalanceのフィールドとなるようにします。一方、アセンブラの方はこのままでは133byte目にbalanceが来てしまい、つまりCの構造体とは異なるモノができあがってしまいます。
左図が元の状態で、右図がaddressフィールドを1byte長くし、アラインメントを入れた場合です。アセンブラでも右図のようにしてC言語の場合と合わせたいので、構造体の定義を
struc Customer
c_id resd 1
c_name resb 64
c_address resb 64
align 4
c_balance resd 1
endstruc
と、align 4
を入れて調整します。
本章の残りは構造体の配列について説明してますが、要はそれも各配列要素となる構造体に対してアラインメントしておかないとダメだよという話です。
原書で学ぶ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プロセスで開けるファイル数の限度に達するのを避けるためです。
原書で学ぶ64bitアセンブラ入門(7)
原書で学ぶ64bitアセンブラ入門(6) - わらばんし仄聞記
上のリンクに続いて、11章、浮動小数点数についての続きです。
Conversion
ある整数長から別の整数長や、ある浮動小数点数長から別の浮動小数点数長。はたまた整数長から浮動小数点数長やその逆といった、つまりはword
からquad-word
への変換やdouble-word
からdouble
への変換など、そういった変換についてです。
整数長から整数長への変換は今までに出てきたmov命令を使って成される為、ここでは浮動小数点数を絡めた内容について扱います。
Converting to a different length floating point
ざっと挙げると以下のパターンがあります
- 1つの
float
からdouble
への変換 - 2つのpackされた
float
からdouble
への変換 - 1つの
double
からfloat
への変換 - 2つのpackされた
double
からfloat
への変換
packされた値の場合、doubleは2つしか入らないのでそれにつられてfloatも2つまでであることはどちらにも共通ですね。
また、これらはいずれもソースにはSSEレジスタかメモリのどちらかが使える一方、デスティネーションにはSSEレジスタしか使用できません。
命令の対応表については下記の通りです。
float -> double | double -> float | |
---|---|---|
scalar | cvtss2sd | cvtsd2ss |
packed | cvtps2pd | cvtpd2ps |
実例はこんな感じになります
cvtss2sd xmm0, [a] ; get a into xmm0 as a double
cvtsd2ss xmm0, xmm0 ; convert to float
Converting floating point to/from integer
浮動小数点数から整数への変換、またその逆についてです。
これについてはpackedでの扱いは無いようです。命令は以下の感じ。
float | double | |
---|---|---|
xmm -> int | cvtss2si | cvtsd2si |
int -> xmm | cvtsi2ss | cvtsi2sd |
表にはxmmとは書きましたが、整数から浮動小数点数への変換の場合、ソースには汎用レジスタかメモリが使えます。
実際の使い方はこんな感じになります
cvtss2si eax, xmm0 ; convert to dword integer
cvtsi2sd xmm0, rax ; convert qword to double
cvtsi2sd xmm0, dword [x] ; convert dword integer
Floating point comparison
浮動小数点数の比較についてです。
IEEE754の仕様では"Not a Number"、つまりNaNについては2つの型があります。quiet NaN(QNaN)とsignaling NaN(SNaN)です。
SNaNは発生すると常に例外を起こし、QNaNは例外を起こさずにその値が安全に伝播していきます。
さて、浮動小数点数での比較の場合、その比較がorderedであるかunorderedであるかによって挙動が変わります。orderedでの比較の場合、オペランドにQNaNかSNaNがあると例外を起こし、unorderedでの比較の場合はSNaNの場合のみ例外を起こします。表にするとこんな感じに。
ordered | unordered | |
---|---|---|
QNaN | 例外 | |
SNaN | 例外 | 例外 |
尚、gccコンパイラはunorderedを使うので、QNaNがオペランドに渡された場合でも例外を起こしません。gccも使ってるということでunorderedについて見てみると、floatを比較するにはucomiss
命令を使い、doubleを比較するにはucomisd
を使います。
これらの比較命令を使うにあたって、第一オペランドはXMMレジスタでなければなりませんが、第二オペランドはXMMレジスタかメモリが使えます。また、これらの比較をする事で各種フラグがセットされることになります。
比較後の条件ジャンプについても、整数での比較後に使っていたjge
などとは異なる命令を使うことになります。ここで使う命令は jb
jbe
ja
jae
となります。aとbはそれぞれ、aboveとbelowの意味のようです。詳細はググれば出てくるでしょうからそちらにお任せ。
Mathematical functions
SSE命令は最小と最大の計算、四捨五入、平方根の計算、逆数平方根についての浮動小数点数関数を持ちます。この節はそれらについて扱います。
Minimum and maximum
最小、最大を計算する命令についてです。一見、命令の名前からしてpackedデータとして複数持つ値の内、最小や最大を計算してくれるのかと自分は思ってしまいましたが、実際には2値の比較しかしてくれません。packedデータの扱いについては後ほど。
とりあえずスカラー用命令については下記のパターンがあります。
float | double | |
---|---|---|
最小値 | minss | maxss |
最大値 | minsd | maxsd |
これについてもデスティネーションはXMMレジスタのみで、ソースはXMMレジスタかメモリが使えるという点は相変わらずです。
packedデータについて、命令語のパターンは今までと同様の変化で使えます。scalarの意味のsであったところをpに変えるだけですね。
さて、packedの場合、packされている2要素間で、それぞれのpack値間で比較をして、それらの大きい方または小さい方の値をデスティネーションのpack値へ入れることになります。packedなfloatの最大値を取得する場合、下図のようになります。
rounding
丸めの対象としてはここまでと同様、floatかdouble、scalarかpackedかの組み合わせに拠る4通りの命令があります。
命令はそれぞれ、roundss
roundps
roundsd
roundpd
となります。
これらの命令は3つのオペランドを取り、この3つめは丸めのモード指定です。モードのパターンは下記のようになります。
mode | 丸め方法 |
---|---|
0 | 最近接遇数丸め |
1 | 正の無限大への丸め |
2 | 負の無限大への丸め |
3 | 0への丸め |
丸め方法の日本語については 端数処理 - Wikipedia を元にしました。
正直この日本語での記述を見たときは1,2の意味がわからなかったですが、これの英語版を見ると
日本語版wiki | 英語版wiki |
---|---|
正の無限大への丸め | toward +∞ |
負の無限大への丸め | toward −∞ |
となっており、それぞれ、正の無限大、負の無限大へ近づく方向へ丸めるという意図がわかります。
最近接遇数丸めは
端数が0.5より小さいなら切り捨て、端数が0.5より大きいならは切り上げ、端数がちょうど0.5なら切り捨てと切り上げのうち結果が偶数となる方へ丸める。
というもので、銀行家がよく使った丸め方でもあるそうで、「銀行家の丸め(banker's rounding)」とも呼ばれるそうです。説明の実例を挙げてみると
- 5.9 -> 6
- 5.2 -> 5
- 5.5 -> 6
- 4.5 -> 4
てな感じで、0.5の値について挙動が分かれるパターンの様です。
Square roots
square rootを求める命令があるってだけの話です。ソースとデスティネーションに使えるレジスタ、メモリの制約やscalar、packedについての変化など、今までの命令と同じです。
命令語は sqrtss
sqrtps
sqrtsd
sqrtpd
があります。
この後の節については単なる実例というかサンプルなので省略。