わらばんし仄聞記

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

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

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点を結ぶ線のうち、直線が最短であることを変分法を用いて示せ

解答

題意より、下のような図を考えます。

f:id:warabanshi21:20141002005744p:plain

与えられる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*}

と表せるので、先ほどの{ \displaystyle {ds} }より

\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の読み込みを実行すると仮定します。

  1. 1byteをバッファから読み込もうとする
  2. バッファに対象のデータが無い
  3. バッファを満たすだけのデータ(一般的に8192byte)をバッファに読み込む
  4. バッファに読み込まれたデータから目的の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の構造体とは異なるモノができあがってしまいます。

f:id:warabanshi21:20140617011735p:plain

左図が元の状態で、右図が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の最大値を取得する場合、下図のようになります。

f:id:warabanshi21:20140522012330p:plain

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 があります。

この後の節については単なる実例というかサンプルなので省略。