Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Acidの使い方(基本)

AcidはPlan 9またはPlan9portで使えるデバッガです。おそらくAcidの用途で最も多いのは、suicide*1したプロセスをアタッチして、stk()lstk()を使って落ちた様子をみてCtl-dで抜ける、ような使い方だと思いますが、Acidはこれ自体がシェルとCの中間みたいなプログラミング言語になっていて、やれることは非常に多いです。が、そこまで書くのは大変なので、今回の記事ではブレークポイントで止めて値を眺めるまでの基本的な使い方を書きました。

使い方

acidコマンドの引数にデバッグしたいファイルを渡すと、まだプロセスが作られていない状態でデバッガが起動します。acid:というプロンプトが表示されるので、引数が必要であればprogargsにスペース区切りで設定してプロセスを作成しましょう。

% acid /bin/git
/bin/git: 386 plan 9 executable
/sys/lib/acid/port
/sys/lib/acid/386

// 引数を設定(必要なら)
acid: progargs = "stash save Debugging"

// プロセス生成
acid: new()
1415: system call  _main       SUBL $0xc,SP
1415: breakpoint   main+0x3   CALL trace2_initialize_clock(SB)

// プロセスの処理を開始
acid: cont()

acidはプロセスIDを与えると直接そのプロセスをアタッチします。この場合は、プロセスはすでに動いているためnew()でプロセスを作成する必要はありません。

% acid <プロセスID>
/bin/git: 386 plan 9 executable
/sys/lib/acid/port
/sys/lib/acid/386

// スタックトレース取得
acid: stk()

Acidの変数

変数はプログラムのシンボルテーブルで扱うものがそのまま参照できます*2。具体的に書いた方が分かりやすいと思うので、以下のプログラムを説明のために使います。

#include <u.h>
#include <libc.h>

typedef struct Addr Addr;
struct Addr {
    char *host;
    int port;
};

char *network = "tcp";

void
main(void)
{
    Addr a;

    a.host = "localhost";
    a.port = 9000;
    print("%s!%s!%d\n", network, a.host, a.port);
    exits(nil);
}

Acidはシンボルテーブルを参照します。このため、デバッグ対象プログラムの変数を参照するには、ローカル変数の場合はfunc:nameのように関数名と変数名を:で繋げてアクセスします。グローバル変数ならnameのように変数名だけでアクセス可能です。

acid: network
0x00006010

ただし、Acidから参照した変数はそのアドレスを表します。Cで言うと(void*)&networkのような扱いです。単項*演算子を使うと、networkが指しているメモリの値を取り出すことができます。ソースコード上では*networkはC文字列へのポインタですが、AcidではCの型情報が失われているので、ただの整数です。

acid: *network
0x00006850

Acidでは、Cの型とは別に変数のフォーマットが存在します。現在変数が持っているフォーマットはwhatisで調べたり、fmtで変更したりできます。そして*演算子がメモリから取り出す値のサイズはAcidの変数が持つフォーマットに依存します。例えばs(文字列)なら\0まで読み込みますし、D(4バイト整数)なら4バイト読みます。

それではnetwork変数が指している文字列を表示してみましょう。

acid: whatis network
integer variable format X
acid: x = fmt(*network, 's') // *network=xが指すアドレスには文字列が格納されている(x自体はただの整数)
acid: x = *network\s         // fmtのショートハンド(*の方が結合強い)
acid: whatis x
integer variable format s
acid: s = *x
string variable
acid: s
tcp
acid: *(*network\s)          // これでもいい
tcp

フォーマットを変更すれば、整数や浮動小数なども読み取れます。*と似たような@演算子もあるようです。利用可能なすべてのフォーマットはAcid Manualを参照してください。

構造体などのメンバ変数

構造体などの場合、Acidは型情報を持っていないのでメンバ変数を名前で参照できません。もちろん変数の先頭アドレスからオフセット分だけ加算すればアクセス可能ですが、アラインメントなどを考慮すると非常に面倒です。8c -aオプションを使うと、Acidで利用できる型を生成してくれるので、これを使うといいでしょう。他のコンパイラではDWARFとして埋め込まれているような情報も、Plan 9では人が読み書きできるようなテキストファイルとして分けて扱います。

# -nオプションを省略するとstdoutへ出力
# -aaオプションの場合は.hファイルの内容を出力に含まない
% 8c -an main.c

-aオプションが生成したファイルには、以下のようにCの構造体や共用体と同じ名前の型や関数が含まれます。

sizeofAddr = 8;
aggr Addr
{
    'X' 0 host;
    'D' 4 port;
};

defn
Addr(addr) {
    complex Addr addr;
    print("  host    ", addr.host\X, "\n");
    print("  port    ", addr.port, "\n");
};

これらのファイルは、acid -lオプションやinclude(string)などで読み込んで使います。

% acid -l ./main.acid 8.out
// キャストする場合
acid: x = (Addr)main:a
acid: x
     host 0x00006854
     port 9000
acid: *(x.host)
localhost

// 関数を使う場合
acid: Addr(main:a)
     host 0x00006854
     port 9000

ブレークポイント設定

ブレークポイントの設定はbpset(address)bpdel(address)関数で行います。acidはシンボルテーブルを参照するので、address引数には関数名をそのまま渡してもいいし、filepc(where)関数でファイル名と行番号から明示的に与えることもできます。また、現在設定中のブレークポイントが知りたければbptab()関数が使えます。

acid: bpset(strbuf_vinsertf)
acid: bpset(filepc("strbuf.c:262"))
acid: bpset(filepc("strbuf.c:274"))
acid: bptab()
    0x00236588 strbuf_vinsertf     SUBL    $0x20,SP
    0x002365ad strbuf_vinsertf+0x25       MOVL    0x8(BP),CX
    0x002366b4 strbuf_vinsertf+0x12c  MOVL    0x8(SI),CX
acid: cont()

ブレーク中の操作

ブレークポイントで停止すると、プロセスはStopped*3状態に遷移して、acidが入力を受け付けるようになります。

ソースコードレジスタの表示

停止している場所を調べるためにソースコードの参照ができます。

acid: src(addr)   // addrを中心に10行Cのソースを表示
acid: line(addr)  // addrの行だけCのソースを表示
acid: asm(addr)   // addrから30行アセンブリのソースを表示

PCレジスタに現在のプログラムカウンタが設定されているので、これを使うのが便利でしょう。

acid: src(*PC)
/tmp/x/git/strbuf.c:274
 269       strbuf_grow(sb, len);
 270       memmove(sb->buf + pos + len, sb->buf + pos, sb->len - pos);
 271       /* vsnprintf() will append a NUL, overwriting one of our characters */
 272       save = sb->buf[pos + len];
 273       len2 = vsnprintf(sb->buf + pos, len + 1, fmt, ap);
>274        sb->buf[pos + len] = save;
 275       if (len2 != len)
 276           BUG("your vsnprintf is broken (returns inconsistent lengths)");
 277       strbuf_setlen(sb, sb->len + len);
 278   }
 279   

演算もできるので現在位置から少し前も見られます。

acid: asm(*PC-10)
strbuf_vinsertf+0x122 0x002366aa   MOVL sb+0x0(FP),SI
strbuf_vinsertf+0x126 0x002366ae   MOVL len+0x1c(SP),BP
strbuf_vinsertf+0x12a 0x002366b2   MOVL AX,BX
strbuf_vinsertf+0x12c 0x002366b4   MOVL 0x8(SI),CX
strbuf_vinsertf+0x12d 0x002366b5   DECL SI
strbuf_vinsertf+0x12e 0x002366b6   ORB  CL,0xa048dea(CX)
strbuf_vinsertf+0x134 0x002366bc   ADDL pos+0x4(FP),AX
strbuf_vinsertf+0x138 0x002366c0   MOVBSX   save+0x17(SP),CX
strbuf_vinsertf+0x13d 0x002366c5   MOVB CL,0x0(AX)
...

レジスタに保存されている値はregs()などで表示できます。

acid: regs()
PC  0x002366b4 strbuf_vinsertf+0x12c  /tmp/x/git/strbuf.c:274
SP  0xdfffe884 ECODE 0xf01006d6 EFLAG 0x00000692
CS  0x00000023 DS   0x0000001b SS 0x0000001b
GS  0x0000001b FS   0x0000001b ES 0x0000001b
TRAP    0x00000003 breakpoint
AX  0x0000000b BX  0x0000000b CX  0x0074571b DX  0x004ebd7c
DI  0xffffffff SI  0xdfffeabc BP  0x00000016

他に、汎用レジスタ浮動小数レジスタだけ表示する関数もありますが、通常はregs()があれば十分でしょう。

スタックトレースの取得

stk()またはlstk()関数でスタックトレースを取得できます。lstk()stk()と似ていますが、ローカル変数の値も含めて出力されます。

ステップ

cont()またはstep()関数で処理を再開できます。

acid: step() // 1命令を実行
acid: cont() // なんらかで停止するまで実行

マルチプロセス

デバッグ中のプログラムがfork(2)した場合、新しいプロセスはStopped状態になって停止します。そのプロセスをデバッグしたい場合は

% acid <新しいプロセスのPID>

でアタッチしてcont()すればデバッグ可能ですし、処理を進めたいだけなら

% echo start >/proc/<新しいプロセスのPID>/ctl

で再開できます。プロセス操作の詳細はproc(3)を読んで下さい。

または、acid:プロンプトで待ち受けている状態であれば、以下の関数が使えるかもしれません。

acid: procs()      // アクティブなプロセスリスト取得
acid: setproc(pid) // pidをカレントプロセスに切り替え
acid: start(pid)   // 停止しているpidを再開

ライブラリ

acid-l libraryオプションを与えると/sys/lib/acid以下*4のライブラリを読み込みます。特にtrussは、実際に発行されたシステムコールを確認できるので便利です。

% acid -l truss /bin/git
acid: progargs = "xx"
acid: new()
acid: truss()

参考情報

*1:Unixで言うところのSEGV

*2:Acidの予約語と被った場合は名前の先頭に$を付ける

*3:suicideした場合はBroken

*4:p9pの場合は$PLAN9/acid

Titan Security Keyを使ってみた

ここ数年で、身の回りでは2段階認証が当たり前になってきたように思います*1。個人的には今まで、2段階認証が可能なサービスにはSMSとモバイルアプリを登録していました。しかしここ最近は、サービスにログインする度にIIJ SmartKeyやGoogle Authenticatorなどの認証アプリを起動して、表示された6桁の数字を入力することをめんどくさいと感じるようになりました。数字入力の手間を、Titan Security Keyではボタン押すだけで解決できそうだったので買ってみました。

Titan Security KeyはFIDO U2Fに準拠したデバイスです。FIDOには1と2のバージョンがあり、WebAuthnはFIDO2に含まれます*2。WebAuthnは後方互換によりFIDO U2F対応デバイスも利用可能なのでTitan Security Keyも使えますがパスワードレスログインはできません。

パスワードの不要な世界はいかにして実現されるのかによると、FIDO2では以下の3要素でデバイスを分類しています。

  • Transport : 接続方法
    • USB
    • BLE
    • NFC
    • ineternal
  • Attachment : デバイスに直接組み込まれているかどうか
    • platform(組み込み)
    • cross-platform(取り外し可能)
  • User Verification (UV) : 生体認証を含めた本人認証機能があるかどうか
    • あり
    • なし

例えばTitan Security KeyまたはApple Touch IDの場合は以下のように分類されます。

基準 Titan Security Key Apple Touch ID
Transport USB or BLE internal
Attachment cross-platform platform
User Verification なし あり

対応サイト

2019年現在、2段階認証の要素にセキュリティキーがサイトは以下のどちらかに対応しているようです。

  • WebAuthn (FIDO2)
  • U2F, Universal 2nd Factor (FIDO1)

これらの違いは、U2Fはあくまで2段階要素に使える仕様ですが、WebAuthnはUser Verificationありのデバイスに限りユーザ名やパスワードの代用も可能になっているところのようです。上にも書いたように、WebAuthnはU2Fデバイスも利用可能です。

使い方

Titan Security KeyはUSB版とBLE版の2つ梱包されています。Googleによると普段は一つだけ使い、もう一つは安全な場所に保管することを推奨しています1。PCやMacだけで使うならUSBの方が使いやすいと思いましたが、モバイル端末でも使いたいのでBLEの方を普段使いに選びました。しかし現状、macOS 10.14ではBLEのペアリングができないので、付属のUSBケーブルでUSBデバイスとして接続する必要があります。また、iOSではSmart Lockアプリを使わなければ接続できません。以下の表は、BLE版を各プラットフォームで接続する方法です。

OS 接続方法 利用可能ブラウザ
macOS USB FirefoxまたはChrome
iOS BLE+Smart Lock Chromeのみ
Android BLE FirefoxまたはChrome

macOSで使う

macOS 10.14時点では、Safari U2FとWebAuthnどちらにも対応していません。そのためFirefoxまたはChromeを使う必要があります。また、システム環境設定のBluetoothからペアリングができないため、USBとして接続する必要があります。

  1. Titan Security KeyをUSBで接続する
  2. U2FまたはWebAuthn対応サイトで2段階認証を設定する
  3. 途中でボタンを押すように促されるのでTitan Security Key中央のボタンを押す(緑色に点滅し始める)
  4. 2段階認証でセキュリティキーが有効になる
  5. 再認証時に、セキュリティキーを選んでボタンを押す

USBとして接続させるためケーブルで繋げなければならない点を除いては、特に不満がありません。ブラウザからBLE機器へ接続するWeb Bluetooth APIが使えるなら事前のペアリングが不要になったりするのかなと期待しましたが、現時点でブラウザの対応状況は今ひとつのようです。

WebAuthnでBLEデバイスを使う方法によると、BlueTooth explorerを使うとBLEデバイスとしてペアリングできるそうですが、今回は試していません。

Androidで使う

利用前に、OSとデバイスの間でペアリング(鍵交換)しておく必要があります。FIDO Bluetooth Specification v1.0に、

Clients and Authenticators MUST create and use a long-term link key (LTK) and SHALL encrypt all communications. Authenticator MUST never use short term keys.

とあるのでペアリングは必須です。

  1. 対応サイトにログインする
  2. 2段階認証にセキュリティキーを使う
  3. ペアリングするように促されるので続ける
  4. Titan Security Keyのボタンを5秒間押して青い点滅になるまで待つ
  5. Androidからペアリングを行う
  6. PINコード(Titan Security Key裏面の数字)を入力する

これでペアリングが完了するので、サイトに戻ってセキュリティキー中央のボタンを押せばログインできます。次からはペアリングされた状態になっているので、ボタンを押すだけでログイン可能です。

BLEのペアリングには、SSP(Simple Secure Pairing)とPINのモードがあり、セキュリティモードで使い分けるようですが詳細は追っていません。

具体例

iOSで使う

設定アプリではTitan Security KeyとBLEペアリングできませんのでGoogle Smart Lockアプリを使う必要があります。

  • Smart LockアプリでGoogleアカウントにログインする(ログインが終われば閉じても良い)
  • ChromeGoogleアカウントの2段階認証を行う
  • Smart Lockアプリが起動するのでペアリングする

あとはAndroidと同じようにセキュリティキー中央のボタンを押せばログインできます。ただし、ChromeGoogleアカウントにログインする以外の用途には(Smart Lockが反応しないので)使えません。とても残念。

Linuxで使う

試していませんが利用可能なようです。

調べたこと

どうやってセキュリティキーからデータを受け取っているか

Androidでいくつか試した限り、セキュリティキーに常時BLE接続しているわけではなさそうでした。だとすると、ブラウザがセキュリティキーを必要とした時に、OSがペアリング済みのデバイスと接続して、ボタンが押されて認証を終えたら切断しているんでしょうか。詳細はわからないけどそんな気がする...

Touch IDや指紋認証と比べてどうなの

基本的に、デバイス内部で持っている秘密鍵を取り出せるべきではありません。WebAuthnの仕様に、

In general, it is expected that a credential private key never leaves the authenticator that created it. Losing an authenticator therefore, in general, means losing all credentials bound to the lost authenticator, which could lock the user out of an account if the user has only one credential registered with the Relying Party.

とあり、秘密鍵のバックアップを可能にするよりも複数のデバイスを登録可能にするよう書かれています。

そうすると、Attachmentがplatformなデバイスは端末に紐づくため、新しい端末を購入した場合は個別に設定が必要ということでしょうか。スマホを1〜2年に1回移行するように仮定すると、その度に各サイトへログインして、新しいデバイスの設定追加が必要になるのは少し面倒な気がします。サイトが数個程度なら苦ではないけれど、GitHub, Google, Twitter, AWS, ...など非常に多いし今後も増えることが推測できるので、ユーザIDやパスワードの入力することになっても、cross-platformなセキュリティキーを使ったほうが便利かなと思いました。

セキュリティキーはいくつまでペアリング可能か

1つのセキュリティキーを何台のデバイスからペアリング可能かは分かりませんでしたが、少なくともTitan Security KeyはAndroidiOSの2台までは確認しました。

開発

WebAuthn APIを使うときの情報。


  1. Titan Security Key をセキュリティキーに使うによると「両方とも2段階認証に設定しておき、片方は安全な場所に保管しておく」が正しいようです。

*1:多要素認証とも呼ぶけどこの記事では2段階認証という名称を使う

*2:iOSのFIDO対応についての考察を参照

カーネル拡張をApple Notary Serviceにアップロードする

個人的にメンテしているHHKPS2USBDriverというカーネル拡張を、Apple Notary Serviceから証明してもらえることができたので記録に残します。

どんな拡張なのか

古いPS/2接続のHappy Hacking Keyboardは、PS/2をUSBに変換するコンバータを使ってmacOSに接続するとCommandキーを正しく入力することができません。この拡張はコンバータ経由でもCommandキーを扱えるようにキーコードを置き換えます。オリジナル版はNAKANISHI Ichiroさんによって作成されましたが、Yosemite以降はコード署名が必須となり使えなくなっていたので、許可を頂いてメンテと公開を行なっています。

Apple Notary Serviceにアップロードする

macOS 10.14.5から、Notary Serviceで署名することが基本的に必須となりました。これはカーネル拡張も例外ではないので、HHKPS2USBDriverも対応が必要となりました。Apple公式のドキュメントには、Notary Serviceで署名するためにはXcodeからアップロードするように書かれていますが、カーネル拡張プロジェクトの成果物はGeneric Xcode Archiveと呼ばれる形式になるようで、この形式ではXcode Distribute Appオプションが現れないためアップロードすることができません。

f:id:lufiabb:20190605215034p:plain
Distribute Appが無い

Generic Xcode Archiveが作成されてしまう原因は、アプリのArchiveでipaが作れなくて焦ったメモによると、

  • Build SettingsのSkip InstallがNO
  • Build PhasesにHeadersがある

のうち片方でも該当すると対象になるようですが、カーネル拡張の場合は上記のパラメータを設定してもGeneric Xcode Archiveから変わりませんでした。

コマンドラインからアップロードする

Generic Xcode Archiveの場合でも、コマンドラインならApple Notary Serviceにアップロード可能です。公式のドキュメントは以下にありました。

ドキュメントを読めば書いていますが、少し悩んだところがあったので今回の手順をまとめておきます。

  1. Xcodeアーカイブを作成して署名する
  2. OrganizerからDistribute Contentを押してモーダルを開き、Built Productsでカーネル拡張(以下ではHHKPS2USBDriver.kext)を書き出す
  3. ditto -c -k --keepParent HHKPS2USBDriver.kext HHKPS2USBDriver.zipを実行
  4. Apple IDサイトにログインしてアプリ用パスワードを生成する
  5. xcrun altool --notarize-app --primary-bundle-id org.lufia.driver.HHKPS2USBDriver --username '<user@icloud.com>' --password '<passpass>' --file HHKPS2USBDriver.zipでアップロード
  6. 終わるまで待つ(10分程度必要でした)

手順4のアプリ用パスワードは、Using app-specific passwords - Apple Supportに作成手順が書かれていますが、Apple IDにログインして、セキュリティのパスワードを生成を押せばすぐに作成できます。

f:id:lufiabb:20190605215119p:plain
パスワードを生成の場所

また、最後のxcrun altoolは、公式のドキュメントでは@keychain:AC_PASSWORDのようにKeychainのエントリからパスワードを取得していますが、パスワードを直接指定することもできます。今回はローカル環境で実行したため直接書けば問題ありませんでした。ただし、CI環境など不特定多数がログインする環境ならKeychainを使う方がいいでしょう。

上記の手順を終えると、

2019-06-05 13:57:06.788 altool[10401:14566062] No errors uploading 'HHKPS2USBDriver.zip'. RequestUUID = d4db7a94-02b8-4ce9-821b-c4f49d8ba7e5

のようなテキストが出力され、Apple IDに設定しているメールアドレスへメールが届きます。これでプロセスは終わりです。

参考情報

カーネル拡張ではないですが、前職の同僚もNotary Serviceのエントリを書いていました。

また、カーネル拡張を開発する時に調べたリンクや自分で過去に書いた記事などです。

Info.plistOSBundleLibrariesは、ビルドしたカーネル拡張をkextlibsで調べると分かります。

$ kextlibs -xml HHKPS2USBDriver

公式Gitのソースコードレイアウト

Gitのソースコードには、gitコマンドをビルドするためのMakefileが含まれている。Gitコマンドのレイアウトを知らなければ理解の難しいものがいくつかあったので調べた。

Gitコマンドのレイアウト

$PATHに含まれる場所(ほとんどは /usr/bin)にGit関連のコマンドが置かれている。

$ ls -l /usr/bin/git* # コマンド出力は一部加工した
git
git-cvsserver
git-receive-pack
git-shell
git-upload-archive
git-upload-pack

$PATHの他に、Gitが参照するgit-coreというディレクトリがあって、git-coreには$PATHにあるもの全てと、git-addのようにgit-プリフィックスとして付けられた実行ファイルが入っている。git-coreの場所はgit --exec-pathで調べられる。これらのファイルはGitのサブコマンドとして実行できる。実際のところ、ほとんどのファイルはgitシンボリックリンクを張ったものだけど、本体に組み込まれない一部のサブコマンドは、実行ファイルがそのまま配置されている。

$ git --exec-path
/Library/Developer/CommandLineTools/usr/libexec/git-core

$ ls -l $(git --exec-path) # コマンド出力は一部加工した
git -> ../../bin/git
git-add -> ../../bin/git
git-add--interactive
git-am -> ../../bin/git
git-annotate -> ../../bin/git
git-apply -> ../../bin/git
git-archive -> ../../bin/git
git-bisect
git-bisect--helper -> ../../bin/git
...

余談だけどgitコマンドは$PATHgit-coreの2箇所に置かれている。同じものだけど、片方が欠けると動かない。

ソースコードレイアウト

Gitのビルドでは、まずlibgit.axdiff/lib.aなどライブラリをビルドして、Gitの組み込みコマンドとそれらライブラリをリンクするという順番で行う*1

Gitリポジトリ直下にある*.cファイルの一部、例えばdir.cdiff.cなどはlibgit.aとしてビルドされる。このライブラリは、Git本体や、Git本体から独立した単体のサブコマンドとリンクする。リポジトリ直下にあるそれ以外の*.cファイルは、Git本体のソースコードであったり、単体サブコマンドのソースコードであったりする。例えばhttp-fetch.cgit fetchの実装だが、これはGit本体とは別の単体コマンドとしてビルドされる。

builtin/以下にはGit本体に組み込むサブコマンドのソースコードが置かれている。ほとんどの組み込みサブコマンドはbuiltin/以下のソースコードと1 : 1で対応している。例えばbuiltin/add.cgit addコマンドそのものであり、cmd_add(argc, argv, prefix)という関数がエントリポイントとなる。これらのファイルは最終的にGit本体に組み込まれて、git-coregit-addとしてシンボリックリンクが作られる。組み込みサブコマンドのシンボリックリンクはどれもGit本体を参照しており、Gitは実行時のファイル名によって、どのサブコマンドを実行するかを決定する。ただし、少数ではあるがbuiltin以下に対応するファイルを持たない組み込みサブコマンドも存在する。例えばgit cherry-pickbuiltin/にファイルを持たないが、git-cherry-pickという名前のシンボリックリンクは用意される。

残りのディレクトリはそれぞれの目的で用意されている。xdiff/xdiff/lib.aソースコードcompat/は各プラットフォームの互換性問題を解消するためのソースコードが格納されている。またDocumentation/には詳細なドキュメントが用意されているので眺めてみると面白いと思う。

Makefileの変数

Makefileでは上で説明したファイルをどう扱っているか。Makefile変数を調べた。

LIB_OBJS

  • libgit.aを作るためのファイルリスト

BUILTIN_OBJS

  • 組み込みサブコマンドのファイルリスト
  • Git本体はこれとgit.cと各種ライブラリで作られる

BUILT_INS

  • Git本体の組み込みサブコマンドリスト
  • BUILTIN_OBJSと、git cherry-pickなどファイルを持たないもの全て

PROGRAM_OBJS

PROGRAMS

GITLIBS

  • Git本体や単体サブコマンドとリンクするファイル
  • libgit.axdiff/lib.acommon-main.o

BINDIR_PROGRAMS_NEED_X, BINDIR_PROGRAM_NO_X

  • 直接$PATHに置かれる単体コマンド
  • Windowsの場合に.exeを付けるかどうかで変数を使い分ける

*1:libgit.alibgit2とは全く別のものでGitからリンクするためだけに使う

Acmeをプログラムから操作する

Acmeは、Plan 9のために書かれたテキストエディタです。現在はPlan 9 from User SpaceUnix環境にも移植されています。

このエディタはプログラムから操作するための機能がいくつか用意されていて、それを使うとキーボードやマウスなどの入力イベントを外部のプログラムが受け取り、加工してエディタに反映するなどといったことができます。標準で用意されているのはメーラーですが、最近Russ CoxがTodoを公開していました。

プログラマ向けの情報はAcmeのマニュアルにあります。

また、Goから扱うためのパッケージもあります。いろいろ複雑なeventファイルを適切に扱ってくれるので便利です。

基本

Acmeは水色の領域(tag)と黄色の領域(body)を合わせてwindowとして扱います。Acmeはwindow毎に1つディレクトリを用意していて(以下の実行例では1という名前のディレクトリ)、windowを開くたびに2,3...と増えていきます。プログラムからAcmeを操作する場合は、これらのディレクトリを使って行います。

$ 9p ls acme | mc
1       cons    draw    index   log
acme    consctl editout label   new

$ 9p ls acme/1 | mc
addr    ctl     editout event   tag     xdata
body    data    errors  rdsel   wrsel

開いているファイルの取得

acme/indexファイルにも、現在開いているファイルやディレクトリのリストが記録されています。acmeディレクトリを直接読み込んでも同じことはできますが、ほとんどの場合はこちらを使ったほうが簡単でしょう。

$ 9p read acme/index
          1          35         318           1           0 /Users/lufia/ Del Snarf Get | Look 
          2          39          20           0           1 /Users/lufia/.inputrc Del Snarf | Look 
          4          39          26           1           0 /Users/lufia/lib/ Del Snarf Get | Look 
          5          40         101           0           0 /Users/lufia/lib/npmrc Del Snarf | Look 

1行が1つのwindowを表し、左から順番に固定幅で次のような情報が書かれています。

  1. windowのID
  2. タグ行の文字数(rune)
  3. ファイルの文字数(rune)
  4. 開いているファイルがディレクトリかどうか(ディレクトリなら1)
  5. 開いているファイルが編集中(未保存)かどうか(編集中なら1)
  6. タグ行の内容

テキストの編集

windowディレクトリ(1/など)配下のaddrdataは、編集しているテキストを読み書きするために使います。

addrAcmeが扱うアドレスを書くとテキストが選択された状態になります。不要な改行を加えるとエラーになるので気をつけましょう。Acmeは以下のような書式をアドレスとして扱います。

  • 2 - 2行目を選択
  • 2,4 - 2〜4行目を選択
  • /func.*\n/ - funcから改行までを選択
  • #0,#2 - ファイルの先頭から2文字選択

範囲選択が成功すると、dataxdataの内容が範囲で選択された部分に置きかわります。datareadすると、範囲選択部分が終わってもテキストが続けば最後まで読んでしまいますが、xdataは範囲の終わりでEOFとなります。同様にwriteすると選択した範囲を新しいテキストで置き換えます。

$ 9p read acme/2/body
1行目
2行目
3行目
$ echo -n '2' | 9p write acme/2/addr
$ 9p read acme/2/data
2行目
3行目
$ echo -n '2' | 9p write acme/2/addr
$ 9p read acme/2/xdata
2行目

ただし、addrを更新しても内部的に選択された状態になるだけで、画面とはリンクしていません。画面を更新する場合はctlを使います。

$ echo -n '#30,#45' | 9p write acme/1/addr
$ echo -n 'dot=addr' | 9p write acme/1/ctl

dot=addrは変わったコマンドですが、dotは画面で選択されている範囲、addraddrファイルの内容を表すので、addrファイルの内容で画面の選択範囲を更新する意味になります。画面で選択した範囲をaddrに設定するaddr=dotもあります。

イベントの取得

Acmeのイベントは大きく2種類あります。ひとつはエディタ全体でのイベント、もうひとつはwindow単位で発生するイベントです。

acme/log

logはwindowのID、操作、ファイル名の3つが連続したテキストです。ファイル名は省略される場合もあります。実行例を示します。

$ 9p read acme/log
1 focus /Users/lufia/
2 new     # Newコマンドを2ボタンで実行した場合
2 focus 
3 new /Users/lufia/src/    # 3ボタンでファイルを開いた場合
3 focus /Users/lufia/src/
2 del 
3 focus /Users/lufia/src/
3 del /Users/lufia/src/
1 focus /Users/lufia/

スペースで区切った3つのうち、最初のカラムはAcmeのwindowを表す数字です。2つ目は操作を表すコマンドで、これはいくつかあります。

op 意味
new 新しいwindowを作成した
focus windowにマウスポインタが移動した
del windowを削除した
get ファイルを再読み込みした
put ファイルを保存した
zerox windowを複製した

最後のカラムは開いているファイル名またはディレクトリ名です。

acme/<id>/event

eventはwindow単位のイベントが流れてくるファイルです。このファイルをopenしている間は、イベントが流れてくる代わりにAcmeデフォルトの動作は止まります(書き戻すことでデフォルトの動作を起こせる)。

ファイルの内容は1つのイベントが1行になっていて、エディタを操作するたびに1行追加されていきます。eventファイルを開いてからのイベントは読み込みできますが、過去のイベントを読むことはできません。

[origin][type][addr0] [addr1] [flag] [n] [text?]\n

originは何からイベントが発生したかを表す1文字です。

origin どこからイベントが発生したか
E bodyまたはtagファイル
F eventファイルへの書き込みなど
K キーボード入力によるイベント
M マウス操作によるイベント

typeは大きく4つですが、tagまたはbodyのどちらで発生したものかを区別します。また、typeによって続きのaddr0addr1が意味するものが変わります。

type 意味
D bodyからaddr0addr1の範囲が削除された
d tagからaddr0addr1の範囲が削除された
I bodyaddr0addr1textを挿入した
i tagaddr0addr1textを挿入した
L bodytextを検索(マウスボタン3)
l tagtextを検索(マウスボタン3)
X bodytext実行(マウスボタン2)
x tagtextを実行(マウスボタン2)

Dまたはdイベントの場合、addr0addr1の値は削除前の範囲を指します。そのため、イベント発生後に範囲の内容を読み込んでも、元のテキストは消えてしまっているため取り出せません。また、Iまたはiイベントの場合は、addr0addr1はテキストを挿入し終えた後の範囲(新しく増えたテキスト部分)を表します。では2文字以上範囲選択した状態でテキスト入力するとどうなるか、ですが、この場合はDIのイベントに分割されます。従って、addrdataファイルを使って以下の操作を行なった場合、

$ echo -n 2 | 9p write acme/12/addr
$ echo hello | 9p write acme/12/data

eventファイルには分割された次のイベントが届きます。

FD6 12 0 0
FI6 12 0 6 hello

イベントの残り要素、flagtextは複雑なのでacmeのマニュアルを読んでください。textは色々な要因により省略されたりします。

利用例

AcmeのファイルAPIを使った例をいくつか挙げます。

# 4行目から9行目の範囲をaddrに設定
echo -n '4,6' | 9p write acme/<id>/addr

# 選択した範囲(4行目から9行目)を読む
9p read acme/<id>/xdata

# 先頭からのオフセットで範囲選択
echo -n '#34,#54' | 9p write acme/5/addr

# 選択した範囲を更新
echo test | 9p write acme/<id>/data

# addrの内容をwindowに反映(dot)
echo -n 'dot=addr' | 9p write acme/<id>/ctl

# windowで選択した範囲をaddrに設定
echo -n 'addr=dot' | 9p write acme/<id>/ctl

# 変更ありの状態にする
echo -n dirty | 9p write acme/<id>/ctl

# 変更ありフラグを落とす
echo -n clean | 9p write acme/<id>/ctl

Plan 9 ANSI/POSIX環境での環境変数

Plan 9ネイティブのCライブラリはPOSIXに準拠しておらず、独自の習慣がある。例えばfopenfcloseなどは使わずopencloseシステムコールを使う。バッファリングが必要であればBiobufを使いなさいという態度を取る。Goの標準パッケージに名残が残っているので、知っていれば雰囲気は伝わると思う。*1

Plan 9環境変数/env以下にファイルとして提供されていて、プロセス毎に異なる内容が扱える*2。これらの変数は配列もサポートしていて、配列の場合は\0で区切られたバイト列として表現される。例えばhome='/usr/lufia'の場合、/env/homeには"/usr/lufia"と保存されていて、path=(. /bin)('.'と'/bin'の配列)の場合/env/pathの内容は".\0/bin"となる。

ANSI/POSIX環境

Plan 9にはネイティブ環境の他に、Unixツールを移植するためだけにANSI/POSIX環境(APE)も用意されている。このライブラリはネイティブとは分けて、ヘッダは/sys/include/ape/に、アーカイブ/386/lib/ape//amd64/lib/ape/などアーキテクチャ毎に置かれている。POSIXでは、getenvputenvで設定した環境変数environ配列からも同じ内容が参照できなければならないという制約があり、environ配列は"home=/usr/lufia"のような"name=value"形式の文字列配列と定義されているため、ANSI/POSIX環境では/envを使わずメモリ上の配列を操作する方針をとる。だからgetenvenviron配列を検索するだけの関数で、putenvenviron配列を更新するだけの関数として用意されている。*3

他のプロセスへ環境変数を引き渡す時は、単純にexecveの引数にenviron配列を渡すだけで良い。execveは渡されたenviron配列を使って/envを再構築する。APEライブラリを使ったプログラムは、mainを実行する前に/envenviron配列へ読み込む。こうするとPlan 9ネイティブなプログラムは/envを参照すればよいし、APEライブラリを使って書かれたプログラムでは引き続きenviron配列として参照できるようになる。これらの処理は以下のソースコードに書かれている。

  • /sys/src/ape/lib/ap/plan9/_envsetup.c
  • /sys/src/ape/lib/ap/plan9/execve.c

ただし、Cの文字列は\0を文字列の終わりと扱ってしまうため、ネイティブ環境で配列となっている環境変数をそのまま扱うと(1つ目の\0で終わってしまって)2つ目以降の要素を扱うことができない。ネイティブ環境の場合は/env以下にファイルがあるため、stat等でファイルサイズを取れば長さが分かるけれども、environ配列はCの文字列になってしまうため、本来の長さを知る手段がない。なので_envsetup\00x1に置き換えていて、execve/envへ書き戻す前に0x1\0へ戻す実装となっていた。

そういった理由から、Plan 9ANSI/POSIX環境で、getenvを使って得た文字列の途中に0x1が見つかった場合、その文字列は配列として扱ってあげる必要がある。

バグ

余談だけどANSI/POSIX環境のgetenvenviron配列の要素を指すポインタを返す。putenvenviron配列を更新するが、getenvが返したポインタがまだ参照されている可能性があるため、以前の値を変更してはいけない。なので何度もputenvを呼び出ししていると、以前の値で使っていたメモリは消すことも再利用することもできずそのままリークする。setenvも最終的にはputenvと同じなので、同じようにリークする。

Plan 9ネイティブのgetenvmallocした値を返すので、使い終わったらユーザ側で適切にfreeしなければならない。

*1:他にもdialtokenizeなど割と多く残っている

*2:正確には、rforkのオプションにRFENVGRFCENVGがあって、これらを使って環境変数を子プロセスと共有したり分離したりといった操作をすることになる

*3:明らかに動いてなかったので修正した

Goでファイルの存在確認

Goでファイルの存在確認について、インターネットではos.Statの戻り値がエラーかどうかを判定する方法が紹介されていますけれど、os.Statはファイルが存在する場合でもエラーを返すことがあるため、この方法では正しく判定できないケースが存在します。また、複数プロセスが同じファイルにアクセスする場合は、os.Statの直後で、別のプロセスによってファイルが作成されたり削除されたりするかもしれません。正確に存在確認する場合は、存在確認した後に行う処理によっていくつかパターンがありますが、基本は、事前に存在を確認するのではなく、意図しない場合にエラーとなるようなフラグを立てておいて、OSのシステムコールが返したエラーを判定することになります。

Cなどでは、Unix系OSと異なり、Windowsは別の関数とフラグを、Plan 9は別のフラグを使いますが、Go標準パッケージのsyscallはその辺りの違いを吸収してくれているので、以下の内容はそのまま使えます。*1

目的別に紹介

ファイルの存在確認をする目的ごとに、対応方法は異なります。以下ではos.OpenFileを使いますが、Goにおいてはos.Openまたはos.Createは以下の呼び出しと同じなので、同じフラグになるのであればどの関数を使っても構いません。

// os.Open(name)は以下と同じ
os.OpenFile(name, os.O_RDONLY, 0)

// os.Create(name)は以下と同じ
os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)

ファイルが存在すれば読む

例えば設定ファイルがあれば読む場合などです。事前の確認を行わず、ファイルを読み込みフラグで開いて、エラーなら、それを使って原因を調べるといいでしょう。

f, err := os.OpenFile(file, os.O_RDONLY, 0)
if err != nil {
    if os.IsNotExist(err) {
        return nil // ファイルが存在しない
    }
    return err // それ以外のエラー(例えばパーミッションがない)
}
// ファイルが正しく読み込める
return nil

ioutil.ReadFileなどを使う場合も、とりあえず読み込んでみて、エラーなら上と同じように判定すればいいです。

ファイルを作成するが存在した場合は何もしない

ファイルがなければデフォルトの値でファイルを作成する場合などで使います。os.O_CREATEと同時にos.O_EXCLをセットすることで、作成できなかった場合にエラーとなるため、存在していたことを判定したい場合はos.IsExistで確認する必要があります。

f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
    if os.IsExist(err) {
        return nil // ファイルが既に存在していた
    }
    return err // それ以外のエラー(例えばパーミッションがない)
}
// ファイルに書き込み可能
return nil

ファイルが存在すれば読み込むが、なければ新規作成する

ログなどをファイルへ書き込む場合に使うと良さそうです。ファイルの有無によりエラーとなることがないため、エラーの判定は特にありません。

f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
    return err // エラー(例えばパーミッションがない)
}
// ファイルを読み書き可能
return nil

ファイルが存在すれば削除して新規作成する

設定ファイルの更新などで使います。実際はファイルを削除するわけではなく、os.O_TRUNCフラグによってファイルの内容を消去しています。

f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
    return err // エラー(例えばパーミッションがない)
}
// ファイルに書き込み可能
return nil

ioutil.WriteFileはこのフラグと同等です。

ファイルが存在すれば削除する

削除の場合はos.OpenFileの代わりにos.Removeを使いますが、基本はos.OpenFileと同様に、実行してからのエラーを判定します。

err := os.Remove(file)
if err != nil {
    if os.IsNotExist(err) {
        return nil // 存在していないので何もしなくていい
    }
    return err // エラー(例えばパーミッションがない)
}
// ファイルに書き込み可能
return nil

参考情報

アトミックなファイル更新

ファイルの書き込みについて補足です。

上記では、ファイルの内容を更新するパターンを紹介しましたが、ファイルの内容を更新途中で電源が落ちたなどの原因によって、中途半端な状態が発生することがあります。これを避けるために、POSIXでは同一ファイルシステムにおいてrenameがアトミックであることを利用して、新しいファイルの内容を別のファイルとして保存して、全て終わったあとで本来のファイル名にリネームする手法が使われます。これを自分で書くのは意外と大変なので、Goならnatefinch/atomicを使えば良いでしょう。

また、最近のOSにはファイルの書き込みバッファが存在するため、バッファを使わない一部の例外を除いて、bufio.Writer.Flushなどでフラッシュしてもファイルには書き込まれていない状態が起こり得ます。これがどういった原理なのかは以下の記事が分かりやすいと思います。

*1:Windowssyscall_windows.goPlan 9const_plan9.go辺り