hnwの日記

PHPのシンボルテーブルを覗いてみる

PHPのシンボルテーブルというのはC実装のレベルの用語で、その時点で有効な変数テーブルのことを指します。つまり、グローバルスコープならグローバル変数を管理する変数テーブル、関数スコープならローカル変数を管理する変数テーブルの意味になります。


今回、自作エクステンションhnw/php-arraydumperでシンボルテーブルをvar_dump()するだけの関数symbol_table_dump()を実装してみました。下記のように使います。

<?php
function foo($a = 1) {
    $b = 2
    symbol_table_dump();
}
foo("bar");


出力は次のようになります。

array(2) {
 ["a"]=>
 string(3) "bar"
 ["b"]=>
 int(2)
}


このシンボルテーブルは$$varのようなイカれた変数アクセスの際に参照する必要がありますが、そうでも無い限り不要です。通常のローカル変数はコンパイル時に全部解決できてしまうので、通常の変数アクセスのみであれば実行時にシンボルテーブルへのアクセスは発生しないのです。


実際、PHP 5.3以降ではローカル変数に対応するシンボルテーブルは必要になるまで作られません。$$varのような変数アクセスがあったタイミングで内部的にzend_rebuild_symbol_table()が呼び出され、そのスコープに対応するシンボルテーブルが構築されます。


逆に言うと、$$varのような変数アクセスを多用するのは性能面でペナルティがあると言えるかもしれません。もちろん致命的な性能差が出るほどではないでしょうし、そもそもそんな変数アクセスをしたい状況が珍しいとは思いますが。

Windows Subsystem for Linux上でphp-fpmを動かしてみた

最近のWindows 10にはLinuxバイナリをそのまま動かすような仕組みが導入されています。これは Windows Subsystem for Linux (WSL) とか Bash on Ubuntu on Windows (BoW) などと呼ばれているもので、VM実行でなくWindowsネイティブでLinuxを動かすという意欲的な取り組みです。


この環境で最新版のPHPソースコードからビルドし、アプリケーションサーバとして動作させてみました。


本稿執筆時点(2017年5月)ではWSL自体がまだ不安定な印象ですが、nginx+php-fpmを動作させることができました。以下はWindowslocalhost 80番ポートでPHPが動作している証拠画像です。「System」欄のunameの表示にMicrosoftという文字列が入っているのがオシャレですね。



以下、WSL上でnginx+php-fpmを動かすまでの手順を紹介します。

WSLのセットアップ

Bash on Ubuntu on Windows - Installation Guide」に従ってセットアップします。

PHPのビルド

まずはWSL上でPHPをビルドしてみましょう。


コンパイル済みバイナリをaptでインストールしてもいいのですが、普通にビルドできる程度にWSLが安定してるのかな?という興味から自前ビルドしてみました。


まずは必要パッケージをインストールします。

$ sudo apt update
$ sudo apt install build-essential libxml2-dev zlib1g-dev libcurl4-openssl-dev \
    libjpeg62-dev libpng12-dev libmcrypt-dev libreadline-dev libtidy-dev \
    libxslt1-dev libssl-dev libbz2-dev git autoconf


今回はphpenv+php-buildでPHPをビルドします。

$ curl -L https://raw.github.com/CHH/phpenv/master/bin/phpenv-install.sh | bash
$ mkdir $HOME/.phpenv/plugins
$ cd $HOME/.phpenv/plugins
$ git clone https://github.com/php-build/php-build.git


下記の内容を .bashrc追記してシェルを再起動します。

PATH=$HOME/.phpenv/bin:$PATH
eval "$(phpenv init -)"


あとはphpenv経由でPHPをビルドするだけです。

$ PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j4 phpenv install 7.1.4


ビルドにはかなり時間がかかるので注意してください。筆者の手元のマシン(Thinkpad X260、Intel Core i7 2.5GHz)で30分以上かかりました。同じビルドが12インチMacBook(Early 2016、Intel Core m5 1.2GHz)で10分を切ることを考えると、まだお仕事で使えるレベルではない印象です。今後のパフォーマンスチューニングに期待しましょう。


ちょっと遅いことを除けば、ビルド自体は正常に終了します。ここまでは普通のLinuxと大差ありません。

nginx+php-fpmの設定

つぎに、nginxをaptでインストールします。

$ sudo apt install nginx


設定ファイル /etc/nginx/sites-enabled/default の設定のうち、下記部分のコメントアウトを外して有効化します。

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        location ~ \.php$ {
                include snippets/fastcgi-php.conf;
                fastcgi_pass 127.0.0.1:9000;
        }


nginxを再起動します。

$ sudo service nginx restart


php-fpmの方はデフォルトの設定ファイルをコピーしてそのまま使います。

$ cd $HOME/.phpenv/versions/7.1.4/etc/
$ cp php-fpm.conf.default php-fpm.conf
$ cp php-fpm.d/www.conf.default php-fpm.d/www.conf


下記コマンドでphp-fpmを起動します。

$ $HOME/.phpenv/versions/7.1.4/sbin/php-fpm


ここで /var/www/phpinfo.php などを設置すれば、nginx経由でphp-fpmを利用することができます。


ただし、この設定だと1秒に1回下記のエラーが出続けます。getsockopt(2) の実装がまだ不完全のようですね。

[01-May-2017 20:06:06] ERROR: failed to retrieve TCP_INFO for socket: Protocol not available (92)
[01-May-2017 20:06:07] ERROR: failed to retrieve TCP_INFO for socket: Protocol not available (92)
[01-May-2017 20:06:08] ERROR: failed to retrieve TCP_INFO for socket: Protocol not available (92)


TCPでのプロセス間通信に問題があるならunix domain socketにすればいいじゃない、と考えてunix domain socketも試してみたのですが、phpinfo() の結果が途中で切れてしまうようです。こちらも実装が不完全なようで、16KB超のデータがうまく扱えないとか、そんな制限がありそうな挙動に見えました。TCPの方がまだ安定している状況だといえるでしょう。

まとめ

WSLはまだbeta版なので過度の期待をする方が悪いと言えばそうなのですが、まだ性能が出なかったり未実装のシステムコールもあったりで、お仕事で使えるようになる日は遠いかな、という印象を持ちました。


とはいえ、長期的に期待できる技術なのは間違いないところでしょう。さらなる安定と正式リリースが待ち遠しいですね。

アメリカで何年の4月1日がサマータイムだったか調べてみた

日付関連のテストケースを書いていたら、ロサンゼルスの1970年4月1日0時はサマータイムではないけれど、2012年4月1日0時はサマータイムであることに気づきました。何かの間違いじゃないかと思って改めて調べてみたところ、ロサンゼルスで4月1日がサマータイムだった年は以下の5つの時期に含まれることがわかりました。

  • 1918-1919年
  • 1942-1945年(War Time)
  • 1948年
  • 1974-1975年
  • 2007年以降、現在まで


このうち、1番目は第一次世界大戦中に制定された「標準時間法」 (Standard Time Act)によるものです。アメリカでは初のサマータイム導入でしたが、不評のため2年で廃止されたとか(おそらく終戦の影響もあったでしょう)。


2番目は第二次世界大戦時の省エネ対策として通年の「War Time」が実施されたもので、1942年2月9日から1945年9月30日まで継続的に運用されました。


3番目は1948年にカリフォルニアで電力危機があり、その対策でカリフォルニア州のみ3月14日から通年のサマータイムが実施されたようです。このころ各州バラバラでサマータイムを実施していたようですが、開始時期は大半が4月でした。


4番目は第一次オイルショック(1973年)によるもの。1967年から全米でサマータイムが実施されていますが、この開始時期は4月第1週からでした。しかし、オイルショックの影響により1974年は1月6日から、1975年は2月23日からサマータイム開始だったとのこと。


5番目は「包括エネルギー政策法」(Energy Policy Act of 2005)が2007年から施行され、サマータイム開始が4月第1週から3月第2週にズレたことによるもの。


サマータイムについて調べていたはずが、歴史の教科書に載っているような出来事が関係してくるのは面白いですね。

上のリストを作る方法

上記リストは以下のPHPスクリプトを使って求めました。

<?php
for ($i = 1900; $i <= 2017; $i++) {
    $dt = new DateTime("$i-04-01 00:00:00",
                       new DateTimezone("America/Los_Angeles"));
    var_dump($dt->format("c T"));
}


このスクリプトの出力を下記のようにgrepすれば標準時でないもの(≒夏時間)が抜き出せます。

$ php dst-investigate.php | grep -v ST
string(29) "1918-04-01T00:00:00-07:00 PDT"
string(29) "1919-04-01T00:00:00-07:00 PDT"
string(29) "1942-04-01T00:00:00-07:00 PWT"
string(29) "1943-04-01T00:00:00-07:00 PWT"
string(29) "1944-04-01T00:00:00-07:00 PWT"
string(29) "1945-04-01T00:00:00-07:00 PWT"
string(29) "1948-04-01T00:00:00-07:00 PDT"
string(29) "1974-04-01T00:00:00-07:00 PDT"
string(29) "1975-04-01T00:00:00-07:00 PDT"
string(29) "2007-04-01T00:00:00-07:00 PDT"
string(29) "2008-04-01T00:00:00-07:00 PDT"
string(29) "2009-04-01T00:00:00-07:00 PDT"
string(29) "2010-04-01T00:00:00-07:00 PDT"
string(29) "2011-04-01T00:00:00-07:00 PDT"
string(29) "2012-04-01T00:00:00-07:00 PDT"
string(29) "2013-04-01T00:00:00-07:00 PDT"
string(29) "2014-04-01T00:00:00-07:00 PDT"
string(29) "2015-04-01T00:00:00-07:00 PDT"
string(29) "2016-04-01T00:00:00-07:00 PDT"
string(29) "2017-04-01T00:00:00-07:00 PDT"


上のように、タイムゾーン名がPDT(Pacific Daylight-saving Time、太平洋夏時間)やPWT(Pacific War Time、太平洋戦争時間)などと表示されているのがわかります。


PHPの日付関数はTime Zone Databaseを元にしているので、他の言語でも同じ結果を得る方法があると思います。

感想など

調べてみると、省エネ対策としてカジュアルにサマータイムを実施している歴史がわかって面白いですね。サマータイムが身近でない日本人の感覚だと「本当に省エネ効果あるの?」という気もしちゃいますけど、きっと効果があるからやってるんでしょう。


また、州ごとに夏時間の適用状況が違うのもアメリカならではです。上のスクリプトを America/Detroit とか America/Phoenix などで試すと全然違う結果になって面白いです。

PHP 7.1.3で時刻の差を取ると時々1マイクロ秒ズレる

本日はエイプリルフールなので、ウソでも本当でも誰も困らないPHPのバグの話をします。


PHP 7.1.0からPHPのDateTimeクラスでマイクロ秒の扱いを強化しているようで、挙動やコードの変更がチラホラ見受けられます。(参考:「PHP 7.1からDateTimeが現在時刻のマイクロ秒まで見るようになった - Qiita」)


時刻と時刻の差分を扱うDateIntervalクラスでもPHP 7.1.0以降マイクロ秒に対応したようで、DateInterval::formatメソッドもマイクロ秒の表示に対応しているようです。さっそく実験してみましょう。

<?php
$dt1=new DateTime("2000-01-01 00:00:00");
$dt2=new DateTime("2006-01-02 03:04:05.6");
$interval = $dt1->diff($dt2);
var_dump($interval->format("%R%Y-%M-%D %H:%I:%S.%F"));
/* Output: string(25) "+06-00-01 03:04:05.600000" */


パラメータ%Fで差分のマイクロ秒が表示できました。ところで、次のような例を試すと奇妙なことに気づきます。

<?php
$dt1=new DateTime("2000-01-01 00:00:00");
$dt2=new DateTime("2006-01-02 03:04:05.000251");
$interval = $dt1->diff($dt2);
var_dump($interval->format("%R%Y-%M-%D %H:%I:%S.%F"));
/* Output: string(25) "+06-00-01 03:04:05.000250" */


0と251の差なので「000251」と表示されるはずが「000250」となっており、1マイクロ秒ズレた結果になっています。


ドキュメント化もされていない機能なので誰も困らないと思いますが、一応バグ報告しておきました。

ポートノッキングで10秒間だけsshdを公開する設定

先日Twitterに次のような書き込みをしたところ思ったより反応が良かったので、詳細の設定を紹介します。

といっても特殊なことをしたわけではなく、knockdでポートノッキングの設定を行い、iptablesと組み合わせて実現しました。

ポートノッキングとは

ポートノッキングというのは、決められたポートを決められた順番で叩くことでファイアーウォールに穴を空けられるような仕組みのことです。ポートノッキングを使えば、TCPの7000番、8000番、9000番の3ポートにパケットを送りつけると22番ポート (SSH) へのアクセスが許可される、といった設定ができます。

ポートノッキングの実現にはknockdというデーモンを使うことが多いようです。knockdはlibpcapと組み合わせて使う前提のツールで、どのポートもlistenせずにポートノックを監視できるのが特徴です。

iptablesの設定

早速ですが、筆者の行った設定を紹介しましょう。今回の対象サーバはDebian 8ベースでした。まずはiptables関連のパッケージをインストールします。

$ sudo apt-get install iptables iptables-persistent

次にiptablesの設定ですが、TCP22番ポート (SSH) へのアクセスを全てふさぎます(6行目)。ただし、トラブル時の非常用経路(下記の例では 192.0.2.0/24)からのアクセスだけは許可しておき、以降の設定はこちらから行います(5行目)。

$ sudo iptables -t mangle -A PREROUTING -p tcp --dport 443 -j MARK --set-mark 443
$ sudo iptables -A INPUT -i lo -j ACCEPT
$ sudo iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
$ sudo iptables -A INPUT -p tcp -m tcp --dport 22 -m mark --mark 443 -j ACCEPT
$ sudo iptables -A INPUT -s 192.0.2.0/24 -p tcp -m tcp --dport 22 -j ACCEPT
$ sudo iptables -A INPUT -j DROP

TCP443番からのパケットにはPREROUTINGチェインでマークを付けておくように設定し、マークの付いたパケットのTCP22番へのアクセスは許可する、という設定にしてあります(1行目、4行目)。これはknockdの方の設定と組み合わせて利用します。

最後に、次のように設定を保存します。

$ sudo netfilter-persistent save

ポートノッキングの設定

今度はポートノッキングの設定です。まずはknockdをインストールしましょう。

$ sudo apt-get install knockd

次に、/etc/knockd.confを次のように書き換えます。

[options]
	UseSyslog

[SSH]
	sequence    = 53:udp,443:tcp,123:udp
	seq_timeout = 5
	command     = /sbin/iptables -t nat -A PREROUTING -s %IP% -p tcp --dport 443 -j REDIRECT --to-port 22
	tcpflags    = syn
	cmd_timeout = 10
	stop_command = /sbin/iptables -t nat -D PREROUTING -s %IP% -p tcp --dport 443 -j REDIRECT --to-port 22

sequenceはポートノッキングのポートと順序を指定するものです。今回はUDP53番 (DNS)、TCP443番 (HTTPS)、UDP123番 (NTP) という3つのポートを叩くとポートノッキングが成立するよう設定しました。もちろん、これらのポートにはデーモンは立てていません。

commandはポートノッキング成立時に実行するコマンドで、TCP443番へのパケットを22番ポートに転送するようにしました。上で紹介したiptablesの設定と組み合わさることで、443番へアクセスするとsshdが返事を返す状態になるわけです。

この設定はポートノッキング元のIPアドレスに対してのみ有効になっているということにも注目してください( command の指定にある -s %IP% で実現しています)。ポートノッキング成立と同じタイミングで別の誰かが443番ポートに接続しようとしても単にファイアウォールがパケットを落としてしまうだけで、sshdにアクセスされることはありません。

cmd_timeoutstop_commandはポートを閉じるための設定です。この例では、ポートノッキング成立から10秒後に先ほどの443番から22番への転送設定を削除しています。

最後にknockdを再起動します。

$ sudo service knockd restart

これでポートノッキングを行うことで10秒だけSSHアクセスが可能になる、という設定が完成したわけです。

ポートノッキングに利用するポートの選択

ポートノッキングに利用するポートは本来何でもいいのですが、たまに外向きのアクセスが一部制限されている環境があります。社内ネットワークなどで必要なポートのみ開放されている場合もあるでしょうし、最近は多くのインターネットプロバイダで外向きのTCP25番ポートがブロックされているはずです。

そうした観点から、多くの環境で開いているのではないか?と考えて筆者はDNS、NTP、HTTPSという3つを選びました。身近な環境で外向きアクセスが制限されていないのであれば、7000番以降など後ろの方のポートを使った方がいいかもしれません。

.ssh/configの設定

このポートノッキングは手動で行うこともできますが、毎回手動で叩くのは面倒すぎますよね。それに、モタモタしていると10秒経ってSSHのポートが閉じられてしまうかもしれません。

そこで、ポートノッキングするコマンドを.ssh/configに組み込んでポートノッキングを自動で行うようにしてみましょう。

まずはknockコマンドをインストールします。私は手元のマシンはMacなので、次のようにHomebrewでインストールします。

$ brew install knock

下記のように.ssh/configProxyCommandでknockコマンドを指定することで、SSHログインの直前にポートノッキングを行います。

Host host1.example.com
  Port 443
  ProxyCommand bash -c '/usr/local/bin/knock -d 100 %h 53:udp 443:tcp 123:udp; sleep 1; exec /usr/bin/nc %h %p'

knockコマンドの-dはポートを叩く間隔(ミリ秒)です。テザリング回線を使う場合など待ち時間が短すぎるとうまく動作しないことがありますので、サーバ側のログを見ながら調整してください。

こうしておけば、普段はポートノッキングの存在すら忘れて他のマシンと同様に利用できるわけです。

まとめ

以上で、ポートノッキングの順序を知らない人はSSHログインを試みることすらできない、それでいて設定を知っていれば全自動でポートノッキングを行ってSSHログインできるような環境が構築できました。

念のため補足しておくと、ポートノッキングはセキュリティの観点では気休め程度のテクニックです。ポートノッキングに利用するパケットはそのままネットワーク上を流れますから、途中経路でのネットワーク盗聴などがあれば簡単に再現できてしまいます。間違ってもtelnetdなどセキュアでないサービスの公開にポートノッキングを使ってはいけません。

逆に言うと、ポートノッキングは元々セキュアなサービスを隠すような用途でしか使えません。基本的には「カッコいい」以外のメリットがない仕掛けだと言えるでしょう(今回は実益があったので設定したのですが、それは別の機会に紹介します)。

また、個人的にはknockdを業務で利用することはあまりオススメしません。引き継ぎが面倒になるだけでセキュリティ面の効果は気休め程度、と考えればデメリットの方が大きいくらいだと思います。

一方で、個人用サーバであれば無駄な苦労をしても困るのは自分だけですから、面白いと感じたことはドンドンやればいいと思います。こうした遊びを通じてネットワークまわりの知識が増えていけば、いずれ業務にも生きてくるはずです。

PHPの中身をgdbで観察できるようなDockerイメージを作りました

CLI版のPHPgdb上で動かしつつ、内部的なデータ構造を覗き見ることができるようなDockerイメージを作ってDocker Hubにアップロードしました。Docker環境さえあればすぐに動かすことができます。


このイメージを動かせばCのコードを書かなくてもPHP内部のデータ構造を確認することができます。PHPの内部構造を詳しく知りたい、というような人は参考にしてみてください。

準備

Macの人はDocker for Macを用意しましょう。他のOS上でも同様にDockerをインストールしてください。また、イメージの圧縮時サイズが200MB程度ありますので、それなりのネットワーク環境で遊ぶことをオススメします。

起動

Docker 1.10以降ではseccompにより一部システムコールが制限されているため、コンテナ内でgdbによるデバッグができません。期待通りにgdbを動かすにはコンテナ起動時にコマンドラインオプション「--cap-add=SYS_PTRACE --security-opt seccomp=unconfined」を付ける必要があります。

$ docker run -v $(pwd):/work -w /work --rm -it --cap-add=SYS_PTRACE --security-opt seccomp=unconfined yhnw/php-debug:7.1 /bin/bash

遊び方

とりあえずzvalの中身を覗いてみましょう。


まず作業ディレクトリに移動して、適当なPHPプログラムを作ります。今回は中身を見てみたい変数を列挙してvar_dump()してみましょう。

$ mkdir -p work/php-debug
$ cd work/php-debug
$ vim var_dump.php


今回作成したvar_dump.phpは下記のようなファイルです。

<?php
$a = strtoupper("foo");
$b = [ $a, &$a, "FOO"];
$c = $a;
var_dump($b);
$a = 123;
$b[1000] = 1234.5;
var_dump($b);


このディレクトリでDockerイメージ「yhnw/php-debug:7.1」を実行してgdbを起動します。

$ docker run -v $(pwd):/work -w /work --rm -it --cap-add=SYS_PTRACE --security-opt seccomp=unconfined yhnw/php-debug:7.1 /bin/bash
root@536f406bded6:/work# gdb php
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from php...done.


まずPHP関数var_dump()にブレークポイントを設置します。PHPの内部関数はCの世界では「zif_」というプレフィックスがつきますので、zif_var_dumpという関数名になります。

(gdb) b zif_var_dump
Breakpoint 1 at 0x8429a4: file /usr/src/php/ext/standard/var.c, line 205.


では先ほどのPHPプログラムを実行してみましょう。

(gdb) run var_dump.php
Starting program: /usr/local/bin/php var_dump.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, zif_var_dump (execute_data=0x7ffff2214140, return_value=0x7fffffffb0b0) at /usr/src/php/ext/standard/var.c:205
205		if (zend_parse_parameters(ZEND_NUM_ARGS(), "+", &args, &argc) == FAILURE) {
(gdb) n
209		for (i = 0; i < argc; i++) {


実行するとブレークポイントの設定によりPHPプログラム5行目のvar_dump()で停止します。次に「n」をタイプしてzend_parse_parameters()を実行しています。これによりvar_dump()のパラメータがargsに代入されます。


さて、ではvar_dump()の第一引数$bの中身を見てみましょう。

(gdb) printzv &args[0]
[0x7ffff2214190] (refcount=2) array:     Packed(3)[0x7ffff2258300]: {
      [0] 0 => [0x7ffff225fa08] (refcount=3) string: FOO
      [1] 1 => [0x7ffff225fa28] (refcount=2) reference: [0x7ffff2202120] (refcount=3) string: FOO

      [2] 2 => [0x7ffff225fa48] (refcount=0) string: FOO
}


情報量が多いので全部は説明しませんが、$bは3要素の配列であり、ハッシュテーブルを持たない真の配列(Packed)であることがわかります。また、$b[0]の文字列"FOO"の参照カウントは3であることがわかります。これは$b[0]と、$cと、$aの実体($aおよび$b[1]が共有している)の3つから参照されているためです。$b[1]の参照カウントが2なのは$aと$b[1]が同じ実体を参照しているという意味です。


$b[2]の参照カウントは0ですが、これは文字列リテラルを代入していることが原因です。文字列リテラルインターン化文字列になるため、参照カウントおよびGCの対象外なのです(参考:「PHPのインターン化文字列とは何か - hnwの日記」)。


では、次のブレークポイントまでプログラムを再開してみましょう。

(gdb) c
Continuing.
array(3) {
  [0]=>
  string(3) "FOO"
  [1]=>
  &string(3) "FOO"
  [2]=>
  string(3) "FOO"
}

Breakpoint 1, zif_var_dump (execute_data=0x7ffff2214140, return_value=0x7fffffffb0b0) at /usr/src/php/ext/standard/var.c:205
205		if (zend_parse_parameters(ZEND_NUM_ARGS(), "+", &args, &argc) == FAILURE) {


プログラム5行目のvar_dump()の結果が表示され、8行目のvar_dump()で再び停止しました。改めて$bの中身を見てみましょう。

(gdb) n
209		for (i = 0; i < argc; i++) {
(gdb) printzv &args[0]
[0x7ffff2214190] (refcount=2) array:     Hash(4)[0x7ffff2258300]: {
      [0] 0 => [0x7ffff225fb60] (refcount=2) string: FOO
      [1] 1 => [0x7ffff225fb80] (refcount=2) reference: [0x7ffff2202120] long: 123

      [2] 2 => [0x7ffff225fba0] (refcount=0) string: FOO
      [3] 1000 => [0x7ffff225fbc0] double: 1234.500000
}


$b[1000]に値を代入したことで$bが4要素の連想配列(Hash)に変わったことがわかります。PHPは添字が全部数字であっても中身がスカスカになってしまうような場合は内部構造として連想配列を採用します。各要素のzvalのアドレスも書き換わっていることから、データ構造の変更に伴い要素のメモリコピーが発生していることもわかります。仕組み上当然のことですが、PHP 7において当初配列として利用していたものを途中から連想配列として利用するのは非効率なのです。


また、$a[0]の参照カウントが1減って2になっていることもわかります。これは$aの実体が123に書き換わり、参照先が変わったためです。


ちなみにprintzvというのはPHPに標準添付の.gdbinitで定義されている関数で、zvalの中身を見るのに非常に便利です。もしgdbの標準コマンドで$b[0]の中身を見るとすれば、次のような手順が必要になります。

(gdb) p args[0].u1.v.type
$1 = 7 '\a'
(gdb) p args[0].value.arr.arData[0]->val.u1.v.type
$2 = 6 '\006'
(gdb) p (char *)args[0].value.arr.arData[0]->val->value.str.val
$3 = 0x7ffff22649d8 "FOO"
(gdb) p args[0].value.arr.arData[0]->val->value.str->gc->refcount
$4 = 2


まず$bが配列であること(type=7)を確認し、次に$b[0]が文字列であること(type=6)を確認してその値にアクセスしています。zvalでは共用体を多用しているので仕方ないのですが、ちょっと面倒すぎますよね。

シンボルテーブルを確認する

シンボルテーブルを確認する方法も紹介しておきます。グローバルなシンボルテーブル(つまりグローバル変数)は次のように確認できます。

(gdb) print_ht &executor_globals.symbol_table
Hash(10)[0x1368530]: {
  [0] _GET => [0x7ffff2259100] (refcount=2) array:
  [1] _POST => [0x7ffff2259120] (refcount=2) array:
  [2] _COOKIE => [0x7ffff2259140] (refcount=2) array:
  [3] _FILES => [0x7ffff2259160] (refcount=2) array:
  [4] argv => [0x7ffff2259180] (refcount=2) array:
  [5] argc => [0x7ffff22591a0] long: 1
  [6] _SERVER => [0x7ffff22591c0] (refcount=2) array:
  [7] a => [0x7ffff22591e0] indirect: [0x7ffff2214080] (refcount=2) reference: [0x7ffff2202120] long: 123


  [8] b => [0x7ffff2259200] indirect: [0x7ffff2214090] (refcount=2) array:

  [9] c => [0x7ffff2259220] indirect: [0x7ffff22140a0] (refcount=2) string: FOO

}


そのスコープでのシンボルテーブル(=ローカル変数)は次のようにすれば見えることもあります(PHP 5.2までは常にローカル変数に対応するシンボルテーブルが作られていたのですが、PHP 5.3以降は必要なときだけ作られるようです)。

(gdb) print_ht execute_data.symbol_table

Dockerイメージについて

ちなみに今回作ったDockerイメージはオフィシャルのphpイメージのリポジトリをforkして作りました。Travis CIを利用しており、masterにコミットするとCIでDocker Hubへのデプロイまで行うようにしてありますので、参考にしてみてください。


ちなみに5.6と7.0のイメージもデプロイ済みです。

まとめ

Cもgdbも良くわからなくても、PHPプログラム中のvar_dump()のタイミングで止める方法と「printzv」だけで結構遊べるのではないでしょうか。もう少し深く知りたい場合はgdbのマニュアルおよびPHPソースコードのZend/以下を調べてみてください。

2016年をふりかえる

年末なので今年何をしたか列挙していきます。

書いた

2016年ははてなダイアリーに本記事を含め17本の記事を書きました。人気があったのは下記の記事です。


Qiitaでは62本の記事を書きました。今年はPHP以外の内容を多めにしたつもりだったんですが、人気が出るのはPHPの記事が多いですね。


また、会社ブログにも2本記事を書きました。うち1本の翻訳記事が人気を集めました。

参加した

勉強会・カンファレンス発表を3件行ったのを含め、計5回勉強会に参加しました。


AVTOKYOは本当に面白かったです。来年は全然行ったことがない集まりに積極的に行きたいですね。


PHP勉強会は去年に引き続きの主催でした。約1年ぶりの開催でしたが、来年はペースを上げて年2回開催したいと思っています。

意識的にインプットを増やした

今年は過去触ったことがない技術も積極的に触っていこう、という気持ちで色々取り組んでみました。


つまみ食い程度で終わってしまいアウトプットまで至らないものも多かったので、来年はもう少し深掘りする題材を見つけたいです。

個人プロダクトのリリース・メンテナンス

今年5月にVisual Studio Codeのエクステンション「Auto-open Markdown Preview」を作ってMarketplaceに公開しました。


思ったより多くの人が使ってくれているようでissueもボチボチ来ているんですが、現状で自分の用途は完全に満たしているので、機能追加にはあまり気乗りしないのが悩みどころです。


また、継続してメンテナンスしているPHP拡張モジュールのphp-timecopについて2回マイナーバージョンアップを行いました(PHP7対応、マイクロ秒対応)。

OSSへのcontribution

今年はZshにバグレポを送る、ということをしてみました。この内容はZsh 5.3に取り込まれています。


また、PHP本体へのバグレポを2本、pull requestを1本出しました。


php-buildに3件Pull Requestを送ったりもしました。


他にもPHP-doc ML(PHP日本語マニュアルについて議論するML)に修正提案を5件投げました。しばらくML配信が止まっていたんですが、無事に復活してよかったです!

まとめ

今年は別の技術領域に入門して継続的に取り組んでいこうと思っていたのですが、残念ながら実現できませんでした。一方で、PHPについては目鼻が利くせいか、ちょっと調べるとそこそこ面白い話題にたどり着けたような気がします。他の技術領域で同じようなレベル感になりたいものです。


というわけで、来年もがんばるぞー