Hatena::ブログ(Diary)

hnwの日記 このページをアンテナに追加 RSSフィード

[プロフィール]
 

2016年7月2日(土) PHPのround関数とは一体なんだったのか このエントリーを含むブックマーク このエントリーのブックマークコメント

(7/3 14:05追記)Javaに関する記述について誤認があったので盛大に書き換えました。Java 6、Java 7、Java 8それぞれで実装が変わっていたようです。

(7/13 23:55追記)本記事中ではroundを四捨五入と言い切ってしまっています。これは筆者がC99のroundを基準に考えているためですが、言語によっては偶数丸めになっているround関数も珍しくありません。ご注意ください。


PHPのround関数について、ネット上で次のような記述を見つけました。


PHP

四捨五入の計算を間違える唯一の言語として畏れられていましたが、そのバグは治っているかもしれません(治ってないかもしれません)


主要なプログラミング言語8種をぐったり解説 - 鍋あり谷あり


各言語を面白おかしく紹介する内容とはいえ、ずいぶん雑な理解だなーという印象です。ゆるふわな話だけでPHPdisられ続けるのもどうかと思うので、一連のround関数の話題について僕なりの総括をしてみたいと思います。


以下、こんなあらすじで紹介していきます。


  • PHP以外でも四捨五入関数バグっぽい挙動は珍しくない
    • RubyPythonの四捨五入はエッジケースで間違っていた
    • Javaの四捨五入にはバグなのか仕様なのか微妙なエッジケースがJava 7まで存在していた
    • 四捨五入で小数点以下n位に丸める仕様がそもそも難しい
  • PHPの現在の実装は整数への丸めについては他の言語と同じ
    • 当時話題になったround関数の実装は2009年リリースのPHP 5.3.0でリプレース済

経緯など

元ネタを知らない人向けに経緯を紹介します。


僕が2007年にPHPソースコード中の四捨五入関数の実装に気持ち悪いマジックナンバーを見つけて「PHPの奇妙なround関数」という記事にしたところ、なぜかRubyのまつもとさんの日記に拾われてバズったという事件がありました。


問題になったround関数の実装は率直に言ってやっつけ感あふれるもので、disられても仕方ない内容だったと思います。そもそも僕自身も「PHP気持ち悪いよね、ひどいよね」というつもりで記事を書いたわけです。しかし、非PHPユーザーが安全地帯からPHPを叩くためだけに乗っかってくるのに若干イラっとしたので、「お前らが安全地帯だと思ってる土台も実は脆いんだぜ」と言いたいがために他言語の浮動小数点数周りのバグを探してみました。


そうして調べていく中で、浮動小数点数の四捨五入について何個かエッジケースがあることに気づきました。また、各種プログラミング言語中の人が必ずしも浮動小数点数に詳しくないこともわかってきました*1。まずはround関数のエッジケースと各言語の対応状況について紹介します。


四捨五入のエッジケース1:0.49999999999999994

0.5より小さい倍精度浮動小数点数の中で最大の数が0.49999999999999994です。これを四捨五入すると、なぜか繰り上がって1になってしまう実装があります。



上記記事で紹介した時点では、RubyPythonがそのような実装になっていました。これは四捨五入が「引数が正なら0.5足して小数点以下を切り捨てる」という実装になっている場合に発生します。この問題を回避する実装は「引数が正のとき小数点以下を切り捨てて元の数との差が0.5以上なら1.0を足す」です。いやー浮動小数点数って難しいですね。


四捨五入の挙動がマニュアル通りとは言えない処理系PHP以外にもあった、というのはこの例だけ見ても明らかでしょう。


ちなみに、本件はRubyPythonとも最新版では修正済みとなります(かなり前から修正されているはずですが詳細な時期は把握していません)。


四捨五入のエッジケース2:9007199254740991.0

9007199254740991.0を四捨五入するとなぜか繰り上がって9007199254740992.0になってしまう実装があります。この数は倍精度浮動小数点数仮数部全bitが1であるような数になります。詳細は下記の記事をご覧ください。



これも先ほどの0.49999999999999994と同じく、「引数が正なら0.5足して小数点以下を切り捨てる」という実装のときに問題になる例です。上記記事のタイミングではPythonだけが該当しましたが、その直前くらいまではRubyも同様の実装でした。


もちろん、現在ではPythonの実装も修正されています。


Javaの四捨五入にはバグなのか仕様なのか微妙なエッジケースがJava 7まで存在していた

さて、上記2つのエッジケースについてですが、Java 6のround関数は2つとも間違いっぽい方に丸めます。


public class RoundTest {
    public static void main(String[] args) {
        double d1 = 0.49999999999999994d;
	double d2 = 9007199254740991.0d;
        System.out.println("d1: " + d1); // 0.49999999999999994
	System.out.println("round(d1): " + Math.round(d1)); // Java 6: 1, Java7-8: 0
        System.out.println("d2: " + d2); // 9.007199254740991E15
	System.out.println("round(d2): " + Math.round(d2)); // Java 6-7: 9007199254740992, Java8: 9007199254740991
    }
}

しかし、Java6の挙動はバグとも言い切れません。Java 6のマニュアルには下記のような記述があります。


public static long round(double a)


Returns the closest long to the argument. The result is rounded to an integer by adding 1/2, taking the floor of the result, and casting the result to type long. In other words, the result is equal to the value of the expression:


(long)Math.floor(a + 0.5d)


http://docs.oracle.com/javase/6/docs/api/java/lang/Math.html#round%28double%29


先ほどからイマイチな実装として紹介してきた「0.5足して小数点以下を切り捨てる」が内部実装だと書いてありますので、上の挙動は仕様なのかもしれません。この記述だけでエッジケースの分かるJavaプログラマがどれほどいるかは疑問ですが、文書化されていること自体は素晴らしいと僕は当時から絶賛していました。


ところで、Java 7以降のマニュアルからはこの内部実装に関する記述が消えているようです。トラックバック頂いた記事「JavaのMath.roundがバグっていないと言える可能性について - What will be done tomorrow?」によれば、Java 7で実装が変わったタイミングでマニュアルの記述も変わったということのようです。ただ、このタイミングではd1のみ正しく丸めるようになったようで、d2は繰り上がってしまう実装だったようです。これはマニュアルの記述「the value of the argument rounded to the nearest long value」に反していると言えるでしょう。


Java 8でさらに実装が変わったようで、Java 8からはエッジケース2件とも正しい方向に丸めるようになっています。


本件、OSCPUにも依存すると考えられるので環境によって再現できたりできなかったりあると思いますが、MacOSX環境のOracle JDKでの実験結果を添付しておきます。



四捨五入で小数点以下n位に丸める仕様がそもそも難しい


良い関数の条件の一つに「挙動が直感的である」ということがあるように思います。特に言語の標準関数であれば、長々説明しないと使えないような関数は害の方が多いくらいだと言えるでしょう。


その意味で、round関数小数点以下(n+1)位を四捨五入して小数点以下n位に丸める実装は危険です。n=0のとき、つまり普通の整数への丸めについて言えば2進の浮動小数点数でも0.5や1.5などが誤差無く表現できるので問題とはなりませんが、n>0の場合について言えば四捨五入の境界線(0.05や0.005)も丸まった後の数(0.1や0.01)もピッタリ表現できないため、仕様を言語化すること自体が難しいと言えます。


容易に思いつく実装として、10^n倍して整数への丸めを行って10^nで割るというものがあります。しかし、浮動小数点数を不用意に10^n倍するのは誤差の蓄積を生みやすい処理です。実際、次の例ではRubyPythonが直感に反する結果を返してしまいます。


$ ruby -e 'x=5.015; print x.round(2), "\n";'
5.01
$ python -c 'x=5.015; print round(x, 2),"\n";'
5.01

こうした実装に対する問題点の指摘を3年ほど前に記事にしました。



その後、より良い実装はどのようなものか?という議論に発展してshiroさんの素晴らしい見解を読むことが出来たのは良かったと思っています。



詳細は記事を読んで頂くとして、やはり「小数点以下n位に丸める関数の直感的な仕様は存在しない」というのが結論かなと思います。


元々のPHPのround関数も、小数点以下n位に丸める挙動を直感的にしようとして失敗してしまったものです。この件から我々が学ぶべきことは採用された実装のまずさについてではなく、小数点以下n位に丸める仕様を採用したという仕様策定段階の失敗についてではないでしょうか。


余談:その後Ruby小数点以下n位に丸める仕様を採用した

ところで、PHPのround関数に関する議論をしていた2007年当時最新だったRuby 1.8系のround関数には小数点以下n位に丸める指定はありませんでした。つまり、PHPのような悩みは無かったことになります。僕としては、その平和な状態を維持して頂きたいと思っていました。


その後、2008年頃にまつもとさんと飲み会で同席させて頂く機会があり、「Rubyのround関数には小数点以下n位に丸める仕様は絶対に入れない方がいいですよ」的な進言をしたように記憶しています。前後の文脈も何もなくお伝えしたのでキョトンとされていたような気もしますが、僕としてもそんなキモい仕様が採用されちゃう言語はPHPくらいだろうと思っていたので、詳細の話をすることもありませんでした。


ところが、その後Ruby 1.9系のround関数小数点以下n位で丸める仕様が入ってくることになります。僕は経緯を追っていませんが、ユーザーの声に抗えなかったんでしょうか。これまで誰も不満なく使っているようならいいんですけど。


現在のPHPのround関数の実装

現在のPHPのround関数の処理は、問題のあった実装がPHP 5.3.0で改善されてから変わっていません。詳細は次の記事で紹介しています。



この頃からPHPの仕様変更に関する議論RFCと呼ばれるWiki文書ベースで行われるようになり、大きい変更は多くの人の目が入るようになりました。また、仕様がきっちりドキュメントとして残るようになったのも大きな利点です。この丸め処理も下記のRFCベースで議論されており、これを読めば誰でも仕様把握できる状態になっています。



ちなみに、新たな丸め処理では整数への丸めは他の言語と完全に同じでケチの付け所はありません。


一方、小数点以下n位に丸める処理は直感的とは言えません。丸め位置を変えながら2回丸めるような処理になっており、一部エッジケースでバグっぽい挙動をするような、ある意味PHPらしい処理になっています。


とはいえ、先ほどから繰り返しているように小数点以下n位に丸める仕様を採用した時点で失敗だというのが個人的見解です。既に紹介した通り他の言語も誤差上等で実装しているわけで、PHPだけが変というのも違う気がします。


そんなわけで、PHP 5.3.0以降のround関数は他の言語と同等と言ってしまって差し支えないでしょう。


ちなみに当時話題になったround関数が実装されていたのはPHP 5.2系ですが、2009年に5.3系がリリースされてリプレースがはじまり、2011年には5.2系のセキュリティサポートが切れている状況です。完全に昔話という感じですね。


おわりに

四捨五入くらい誰でも実装できるって思うでしょ?意外とそうでもないわけですよ。ホントに。

*1:もちろん平均的には詳しい人が多いですし、スーパー詳しい人もたくさんいます

yukobayukoba 2016/07/03 12:08 Javaを試してみたのですが、Java 1.8.0_91, _92 で
WindowsおよびLinux、Oracle JDKおよびOpenJDKで、
Math.round(d2)は9007199254740992ではなく
9007199254740991になります。

「Javaのround関数は2つとも間違いっぽい方に丸めます。」
ではなく、どちらも正しいのではないでしょうか?

hnwhnw 2016/07/03 13:39 失礼しました。こんな細かいところに修正が入る可能性は低いと思い込んでいたので、手元にたまたまインストールされていた1.7.0_80のみで確認しておりました。確かに私の手元もMacOSXでも1.8.0_92では9007199254740991に丸めます。

また、d1が0に丸まるのは元々正しい方向に丸めてますよね。すみません、完全におかしいことを書いていました。

このあたり本文に追記および修正させて頂きます。ご指摘ありがとうございました。

2016年5月22日(日) 勝手にMarkdownプレビューを開くVS Codeのエクステンションを書いた このエントリーを含むブックマーク このエントリーのブックマークコメント

Visual Studio Code(以下VS Code)、みなさん使ってますか?VS CodeはMicrosoftが開発しているオープンソースエディタです。Electronベースという意味ではAtomと似た存在ですが、Atomより軽いという評判を聞いたりします。


私も最近VS Codeを使い始めました。いまのところMarkdown編集専用という状況ではありますが、便利に使っています。それだけでなく、「Auto-open Markdown Preview」というエクステンションを書いて自分好みの挙動になるようにしてみました。本稿ではその顛末について紹介します。


ちなみに作業はすべてMacOSX上で行いましたが、WindowsでもLinuxでも同様だと思います。


作ったもの

今回、「Auto-Open Markdown Preview」というエクステンションを作りました。Markdownファイルを開いた場合に、自動的にプレビュー画面も開くというものです。


Auto-Open Markdown Preview導入前

f:id:hnw:20160522123147g:image


Auto-Open Markdown Preview導入後

f:id:hnw:20160522123148g:image


大した差では無いように見えるかもしれませんが、個人的な感覚としては凄い便利になりました。Markdown編集するときには常にプレビューしたいに決まっているじゃないですか。


もちろん意見には個人差があります。差があるからこそカスタマイズ性が大事だと思うんですよね。


Auto-Open Markdown Previewのインストール

このエクステンションインストールは簡単で、VS Code上で「F1」を押して「>」を消して「ext install auto-open markdown」とタイプすればインストール候補として出てきます。


f:id:hnw:20160522123146g:image


以下、これを実現するまでの流れを紹介します。


VS Codeのエクステンションとは

すでに紹介した通りVS Codeにはエクステンションの仕組みが用意されており、ユーザーの手でエディタ機能を拡張することができます。エクステンションJavaScriptまたはTypeScriptで書くことができますが、TypeScriptで書いている人が大多数のようです。


また、書いたエクステンションVisual Studio Marketplaceに誰でも無料でパブリッシュすることができます。エクステンションパブリッシュすると上で紹介したようにVS Code本体のエクステンション検索機能でインストール候補として表示されますので、多くのユーザーに簡単に使ってもらえるような仕組みになっています。


エクステンションの作成

VS Codeエクステンションを作るための道具はnpmに登録されています。あのMicrosoftがnpmに自社ツールを登録するなんて…と不思議な気持ちになるのは私だけでしょうか。それはさておき、さっそく準備していきましょう。


$ npm install -g yo generator-code
$ yo code

     _-----_
    |       |    .--------------------------.
    |--(o)--|    |   Welcome to the Visual  |
   `---------´   |   Studio Code Extension  |
    ( _´U`_ )    |        generator!        |
    /___A___\    '--------------------------'
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

? What type of extension do you want to create? (Use arrow keys)
> New Extension (TypeScript)
  New Extension (JavaScript)
  New Color Theme
  New Language Support
  New Code Snippets

指示に従って適当に答えていくとエクステンション名のディレクトリが作られ、その下にひな形が生成されます。また、node_modules以下に必要モジュールがnpm installされます。


自動生成直後の状態では、「Hello World」と表示するだけのextensionが作られています。


$ code --disable-extensions .

上記のようにエクステンションディレクトリをVS Codeで開き、「F5」を押せばエクステンションデバッガ上で動作します。この環境では、ブレークポイントを置いたり変数値を確認したりしながらエクステンション開発ができます。


詳しくは下記URLを参照してください。



エクステンションパブリッシュ

上記の環境で開発が一段落したら、まずパッケージングしてみましょう。


$ npm install -g vsce
$ vsce package

このようにnpmでvsceパッケージをインストールしてコマンドを打てばプロジェクトディレクトリに*.vsixファイルが出来ます。これがVSCodeのエクステンションパッケージになります。これをVS Codeでオープンすればパッケージがインストールされます。


$ code vscode-auto-open-markdown-preview-0.0.1.vsix

Marketplaceへのパブリッシュもコマンド一発です。ただし、Visual Studio Team ServicesMicrosoftの無料リポジトリ&イシュートラッキングサービス、以下VSTS)のアカウントと、VSTS上で発行されるアクセストークンが必要です。


$ vsce create-publisher hnw
$ vsce login hnw
$ vsce publish

詳しくは下記URLを参照してください。



まとめ

VS Codeの挙動に不満があったら自分でエクステンションを作れば解決できるかもしれないこと、またVS CodeのエクステンションをMarketplaceに公開するまでのハードルは非常に低いということを紹介しました。まだまだエクステンションまわりは資料が少ないという印象ですが、Microsoftの公開している資料やVS Code自体のソースコードTypeScript部分が多い)があれば何とかなると思います。


皆さんも便利なエクステンションを書いて公開してみてはいかがでしょうか。

トラックバック - http://d.hatena.ne.jp/hnw/20160522

2016年4月30日(土) 新MacBook (12-inch, Early 2016)を買ったので性能比較してみた このエントリーを含むブックマーク このエントリーのブックマークコメント

先日発表された12-inch MacBookの2代目を衝動買いしました。MacBook Air (13-inch, Mid 2012)からの買い替えです。MacBook Airは不満の少ないマシンでしたが、持ち歩いていたら肩こりがひどくなったので、性能が落ちずに物理的に軽くなるならと考えて買い換えてみました。


スペック比較

両方ともBTOなしの上位モデルです。


MacBook Air
(13-inch, Mid 2012)
MacBook
(12-inch, Early 2016)
CPU 2.0GHz Intel Core i7
(最大3.2GHz)
1.2GHz Intel Core m5
(最大2.7GHz)
Memory 4GB 8GB
SSD 256GB 512GB
Weight 1.35kg 0.92kg

ネット上の情報からするとCPU性能も大差ないと判断して買ったのですが、動作周波数だけで見ると圧倒的に負けているのがわかります。


コンパイル時間でベンチマークテスト

さて、本当に両者の性能に差はないのでしょうか?ベンチマークテスト上は互角でも、体感値とは違う可能性も考えられますので、自分の身近な作業の速度で比較してみたいところです。


というわけで、PHP 7.0.6のビルド時間を測ってみることにしました*1。makeのオプションを-j1と-j4と試してみて、1並列と4並列の場合で比較しました。


MacBook Air
(13-inch, Mid 2012)
MacBook
(12-inch, Early 2016)
相対性能
1並列 12分8秒 11分15秒 +7.3%
4並列 6分44秒 6分30秒 +3.5%


ギリギリですが、新MacBookの方が速いという結果になりました。この程度の差だと条件が少し変わるだけで逆転されそうですが、互角程度であるという点では間違いなさそうです。


上の結果からCPU単体の速い遅いの判断はできません。大物ソフトウェアコンパイル・リンクではディスクI/O性能の差もかなり効いてきます。SSDの性能は新MacBookの方が断然上なので、その差が結果に出ている可能性もありそうです。


1並列より4並列のときの方が両者の差が縮まっている理由としては、新MacBookCPUが過負荷に弱いということが考えられます。IntelTurbo Boostは温度やその他に余裕があるときにオーバークロックする技術なので、ファンレスで動いている新MacBookの方が早く頭打ちが来ても不思議はありません。


実際、他のベンチマーク結果を見ても1コア性能は互角、マルチコア性能は若干落ちる程度のようです。


とはいえ、CPUの動作周波数だけ見ると勝ち目がなさそうに見えたのが、身近な作業で互角かそれ以上の性能だったことに少し安心しました。


参考URL


まとめ

  • MacBook Air (13-inch, Mid 2012) と MacBook (12-inch, Early 2016)の性能は大差なさそう
  • 430gの軽量化に払う金額としては高すぎると思う
    • でも肩こりがツラかったので仕方ない(自分への言い訳)

*1:「phpenv install 7.0.6」の実行時間を比較したので、xdebugその他のビルド時間も含まれています

トラックバック - http://d.hatena.ne.jp/hnw/20160430

2016年4月19日(火) php-timecopをPHP 7対応させてみた このエントリーを含むブックマーク このエントリーのブックマークコメント

自作のPHP拡張であるphp-timecopPHP 7に対応させてみました。この機会に改めてphp-timecopの紹介をしてみます。


php-timecopとは

php-timecopというのは筆者が4年ほど前に作ったPHP拡張で、現在時刻に紐付いた値を返すPHP関数について、基準となる現在時刻を過去や未来の任意の時刻に設定することができるというものです。


以下に簡単な例を紹介します。


<?php
var_dump(date("Y-m-d")); // 今日の日付
timecop_freeze(0);
var_dump(gmdate("Y-m-d H:i:s")); // string(19) "1970-01-01 00:00:00"
var_dump(strtotime("+100000 sec")); // int(100000)

上記プログラム中2行目のtimecop_freeze()関数php-timecopにより導入される新たな関数で、現在時刻を指定された時刻に固定するというものです。timecop_freeze()の引数として0を指定するとグリニッジ標準時1970年1月1日、いわゆるUNIXエポックを指定したことになります。実際、プログラム中3行目4行目の戻り値を見ると、本来なら現在時刻を基準にするはずの関数UNIXエポック基準で値を返していることがわかります。


詳しい使い方

php-timecop拡張を導入すると、以下の組み込み関数について現在時刻をズラせるような実装に差し替えます。ただし、そのままであれば返す値は元の関数と完全に同じです(変わっているようならバグです)。


  • time()
  • mktime()
  • gmmktime()
  • date()
  • gmdate()
  • idate()
  • getdate()
  • localtime()
  • strtotime()
  • strftime()
  • gmstrftime()
  • microtime()
  • gettimeofday()
  • unixtojd()
  • DateTime::_construct()
  • DateTime::createFromFormat() (PHP >= 5.3.4)
  • date_create()
  • date_create_from_format() (PHP >= 5.3.4)

また、次の3つの関数を使って現在時刻を仮想的にズラせるようになります。


  • bool timecop_freeze(int $timestamp)
  • bool timecop_travel(int $timestamp)
  • bool timecop_return()

上の2つの関数を呼ぶと現在時刻を$timestampで指定された時刻にズラします。timecop_freeze()を呼び出すと時刻はズレたままずっと同じ値を返すようになります。一方、timecop_travel()は呼ばれたタイミングで時刻をズラしたあと時計が通常の速度で動きます。timecop_return()を呼び出すと本来の時刻に戻ります。


timecop_travel()を使う場合、現在の実装では秒の単位しかズラせず、マイクロ秒の単位は元のままである点に注意してください。つまり、timecop_travel(0)を呼び出すと仮想時刻は1970-01-01 00:00:00.000000から1970-01-01 00:00:00.999999のどこかになります。タイミングが悪いと、次の処理を行うまでに1970-01-01 00:00:01になっているかもしれません(いずれ直したいとは考えています)。


php.iniで指定できる設定値は2つあります。


  • timecop.func_override
  • timecop.sync_request_time

前者は組み込み関数の置き換えを行うかどうかの指定で、0を指定すると組み込み関数の差し替えを行いません。デフォルトは1です。1を指定した場合、差し替える前の関数をtimecop_orig_mktime()のように「timecop_orig_」というプレフィックスがついた形で呼び出すことができます。


後者はtimecop_freeze()やtimecop_travel()が呼ばれたタイミングで$_SERVER['REQUEST_TIME']を書き換えるかどうかの指定です。0を指定すると書き換えません。デフォルトは1です。


php-timecopの使いどころ

この拡張は現在時刻が絡んだ自動テストに使うと便利です。PHP標準の日付系関数・クラスを使っている場合、現在時刻によって挙動が変わってしまうので自動テスト対象にするのが難しいのですが、php-timecopを使えば既存コードの変更なしにテストできるようになります。日またぎや年またぎといったテストしづらい状況のテストに利用できるかもしれません。


また、サービスの管理者が動作確認をするような場合にも有用です。たとえば、特定の時間になったら特定の表示を行うような処理について、php-timecopを使えば事前に確認することができます。実際に、筆者の勤務する会社ではこの拡張を使って管理者向けの「仮想カレンダー」機能を実現していました*1


PHPで組み込み関数の挙動を変えるには

ところで、php-timecopはどうやって組み込み関数を差し替えているのでしょうか?RubyPythonなどの言語では組み込み関数をユーザー定義関数で上書きすることができます。一方、PHPではPHPプログラム上から組み込み関数を書き換えることはできません。


実は、組み込み関数を動的に書き換えられないというのはpure PHPレベルでの制約であり、PHP拡張にはそのような制限はありません。PHP関数は内部的には関数名をキーにした連想配列で管理されています。この連想配列PHP拡張から操作できるので、これを書き換えれば関数の差し替えを実現できるというわけです。


php-timecopでは特定の関数だけを自前実装したCの関数で差し替えていますが、RunkitAOP-PHPを使えば任意の関数メソッドの挙動変更をPHP関数で記述できます。時刻系関数以外にもテストの悩みがある場合はこれらの拡張を使った方が便利かもしれません。現時点では両者ともPHP 7に未対応ですが、のんびり待っていれば公式対応が来るんじゃないかな?と個人的には思っています。


PHP 5対応のPHP拡張をPHP 7対応に書き換えるコスト

PHP 7に移行したいけれども使っている拡張がPHP 5にしか対応していない、という悩みを持っている方がいらっしゃるかもしれません。PHP拡張のPHP 7への移行はそんなに大変なのでしょうか?


個人的な感覚として、PHP拡張の書き換えのコストは元のコード量に依存します。PHP 5とPHP 7とでは内部的なAPIが大幅に変更されているため、大物の拡張だと移行は大仕事です。PHP 7の方が内部APIが整理されており、バグを作り込みにくくなったのがせめてもの救いでしょう。


一方で、内部構造の変更は予想より少ないと感じました。zval周り・ハッシュ周りは大幅変更されていますが、それ以外の変更量はそれほど大きくありません。慣れてくるとPHP 7への書き換えは単純作業のような気がします。


ちなみに、php-timecopのPHP 7対応では#ifdefなどによる分岐をあきらめ、ソースコードとして別管理としました。差分に興味がある方はリポジトリ上のtimecop_php5.cとtimecop_php7.cとを見比べてみてください。


まとめ

PHP 7対応を機に、php-timecopについて紹介しました。このところ自分では全く触っていなかったのですが、想像以上に使われているようで驚いています。ご意見・ご要望などあればお気軽にどうぞ。


参考URL

*1:新しい案件では賢い時刻クラスが使われていましたが、古い案件に後から導入するのに活躍しました

トラックバック - http://d.hatena.ne.jp/hnw/20160419

2016年3月27日(日) DNSSECを利用してゾーン情報を抜き出してみた(NSEC編) このエントリーを含むブックマーク このエントリーのブックマークコメント

さいきんDNSSECについて調べているのですが、普及について懐疑的な人が多いという印象を持ちました。批判点として運用の難しさを挙げる人も多く、仕様策定時に運用負荷の観点が入っていたらまた違ったのかなと感じたりもします。


また、運用面の視点以外に、技術仕様面でも批判的な人が一定数いるように思います(DJBが筆頭ですかね…)。批判の一つがゾーン情報の列挙(Zone enumeration)に関するものです。特に、初期のDNSSECの仕様では「NSEC walking」により完全なゾーン情報を第三者が取り出すことができました。これを改良するためNSEC3レコードが導入されたという経緯がありますが、今でもNSECで運用されているゾーンが一定数残っていたりもします。


本稿ではNSEC walkingによりゾーン情報を読み出す方法を紹介します。


NSECレコードとは

NSECレコードというのはDNSSECで導入された新しいレコードタイプで、問い合わせに対して不存在を回答するためのものです。


そもそもDNSSECは、公開鍵暗号を使ってDNS問い合わせの回答に署名するような仕組みです。この仕組みがあれば、存在するレコードについては署名により正当性の確認ができます。しかし、レコードが存在しない場合に従来のDNSでは何も返さない仕様だったので、署名する対象がありません。不存在に対して署名するために、存在しないことを意味するNSECレコードが新設されたというわけです。


このNSECレコードは「aaa.example.comからbbb.example.comの範囲には何もないよ」というような返事をする仕様になっています。すべての問い合わせに対して個別に「それは存在しないよ」と返事を返しているとキリがないので、キャッシュ効率を考えてこのような仕様にしたのだと思われます。しかし、このような仕様になっていることで、先ほどの例なら「aaa.example.com」と「bbb.example.com」という2つのレコードがありそうだということが外部から丸わかりになってしまいます。


もちろん、ゾーン内にどのようなレコードがあるか知られたからといって即座に危険というものではありません。とはいえ、ホスト名にミドルウェア名やアプリケーション名をつけることは珍しくないはずですし(例:redis, irc)、Webサーババーチャルホスト設定をしている場合など、正しいホスト名を知らないとアクセスできないサービスもあるわけです。こうした例からも明らかなようにホスト名を知られることは攻撃リスクの増加につながるため、不用意にゾーン情報を公開しないのが一般的なノウハウです。ところが、レコードの不存在に対してNSECレコードを返してしまうとゾーン情報を全世界に公開するのと同じ状態になってしまうのです。


この問題はNSECレコード導入直後から指摘されていたようで、2008年にはこの問題を解決する目的のNSEC3レコードRFCに採択されています。また、現在では多くのDNSサーバがNSEC3レコードを利用して運用されています。実はNSEC3レコードにも問題点はあるのですが、その話題は別の記事でまとめたいと思います。


digコマンドでNSECレコードを確認してみる

現在ではNSEC3レコードで不存在を回答するDNSサーバの方が多数派ですが、まだまだNSECレコードも現役です。digコマンドで.moeドメインの問い合わせをしてみましょう。


$ dig a zunopan.moe +dnssec +nocmd

; <<>> DiG 9.10.3-P3 <<>> a zunopan.moe +dnssec +nocmd
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 21684
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 6, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 4096
;; QUESTION SECTION:
;zunopan.moe.			IN	A

;; AUTHORITY SECTION:
moe.			6665	IN	SOA	ns1.dns.nic.moe. hostmaster.neustar.biz. 72673 900 900 604800 86400
moe.			6665	IN	RRSIG	SOA 8 1 7200 20160426103703 20160327093703 20989 moe. KkV+vuFD/V36pDolnSIwSXZ24bh+LZHjRBfo44g/I9XL5HbZnfoQZ6nn On0GQcVHbjPKmwKuncdBJ3YEvNP1fmmIe85tznIVm8TvVugc+fgQRe1F v+ShBeY6n84r3ZNc+IOG4vMJ6jJgWEjXPCBwfQjUOISC6cFTOCWUAioZ Ais=
ZUNKO.moe.		83996	IN	NSEC	ZUO.moe. NS RRSIG NSEC
ZUNKO.moe.		83996	IN	RRSIG	NSEC 8 2 86400 20160406151432 20160307144911 20989 moe. ab2jy3xntU87B8eSGONcspcC8yx2joTnfo7fBg445F4C8QlyIIJ8Hiqr K39JwpWcgHrekqgqdc6CGHuRbPtNM1Xq+Qclo28D3Fo5WMuajzBjzZBR DNzTMclkMSWotYP80ksA6vXp7NMbEYCRZo9mcfDKSlOojvjv12E0s4E0 7/8=
moe.			83983	IN	NSEC	0-0.moe. NS SOA RRSIG NSEC DNSKEY
moe.			83983	IN	RRSIG	NSEC 8 1 86400 20160405140613 20160306131312 20989 moe. DEmFsg1au3cB3Xr7xKJt8j+1HkqTGaIKdVXVv6AdrEyAmJooBrQ57oZb eQO+rRfyBuxLsXOaU7aLHfdepTLlphmVuw3tP3rXd4U8JzJaDWFThtZC 9AiA6/UtScSbh6QLfJpQaXFqEHXecsAGFtjmxo9IkADRG3M7tFdMeYP+ uyc=

;; Query time: 129 msec
;; SERVER: 192.168.48.1#53(192.168.48.1)
;; WHEN: Sun Mar 27 19:50:31 JST 2016
;; MSG SIZE  rcvd: 664

上記のうち、下から2行目のNSECレコードが今回話題にしているレコードです。これによれば、「zunopan.moe」は存在しないけど、「ZUNKO.moe」というレコードがあり、対応するレコードタイプは「NS RRSIG NSEC」の3つだということ、その次のレコードは「ZUO.moe」であることがわかります。これを辿っていけばゾーン内の全レコードを取り出せてしまうというわけです。


ちなみに、一番最後のRRSIGレコードが対応する署名になります。参考まで。


NSEC walkingを行うツール

これまで説明してきたような性質を利用してゾーン内の全レコードを取り出すことをNSEC walkingなどと言います。これを行うツールは下記のように何種類か作られています。ちなみに3つめは筆者が今回Go言語の練習がてら書いてみたものです。



筆者の作った「go-dnssec-walker」の実行例を示します。当然ですが、他のツールでも同じような出力が得られます。


$ walker -s zunko moe
zunko.moe.
zuo.moe.
zxz.moe.
zyii.moe.
zyon.moe.
zzz.moe.

このノリで全域を調べると、たとえば.moeドメインには現時点で5075ドメインが登録されているらしい、なんてことがわかったりします。


NSEC walkingできるTLD

今回NSECについて調べてみて、現在もNSECで運用されているgTLD・ccTLDが下記の通りたくさんあることがわかりました。下記のTLD内のドメインは赤の他人が簡単に一覧を取得できるというわけです。他人に知られて困るようなドメインを持っている人は気をつけた方がいいかもしれませんね。


gTLD
  • .accountant
  • .audio
  • .auto
  • .best
  • .bible
  • .bid
  • .biz
  • .blackfriday
  • .buzz
  • .car
  • .cars
  • .ceo
  • .christmas
  • .click
  • .club
  • .cricket
  • .date
  • .diet
  • .download
  • .earth
  • .faith
  • .flowers
  • .game
  • .gift
  • .guitars
  • .help
  • .hiphop
  • .hosting
  • .how
  • .jetzt
  • .juegos
  • .link
  • .loan
  • .lol
  • .moe
  • .mom
  • .nyc
  • .osaka
  • .party
  • .photo
  • .pics
  • .property
  • .qpon
  • .racing
  • .review
  • .science
  • .sexy
  • .soy
  • .taipei
  • .tattoo
  • .trade
  • .uno
  • .webcam
  • .whoswho
  • .win
  • .みんな(.xn--q9jyb4c)
  • .世界(.xn--rhqv96g)
ccTLD
  • .ad
  • .bg
  • .br
  • .ind.br
  • .co
  • .com.gn
  • .id
  • .co.id
  • .or.id
  • .web.id
  • .kg
  • .ky
  • .lk
  • .mg
  • .com.mm
  • .org.mm
  • .na
  • .pr
  • .biz.pr
  • .com.pr
  • .net.pr
  • .org.pr
  • .se
  • .sl
  • .com.sl
  • .tn
  • .com.tn
  • .us

まとめ

  • DNSSECの不存在をNSECレコードで返すのはゾーン情報の公開と同じ
  • どうせDNSSECやるならNSEC3で返した方が多少はマシ

飽きていなければ次回はNSEC3編を書きます。

トラックバック - http://d.hatena.ne.jp/hnw/20160327
 
ページビュー
1879835