興味が湧いたのでPerlのC拡張であるXS言語で書かれているList::Utilを読んでいるのだけど、C言語チックなものを読むときはやはりデバッガが使いたい。
XSも結局C言語なのでgdb/lldbでデバッグできないかなと思ってググったところ、できそうなエントリを見つけたので試していく。
stackoverflow.com
C言語をデバッグにはCで書かれたコードにデバッグオプションを付けてビルドする必要がある。
XSも同様だと思われるので、何かしらの方法でデバッグビルドする必要がある。
XSモジュールに限らずPerlモジュールのビルドは、Makefile.PLかBuild.PLのどちらかのビルドツールがよく使われている。
Makefile.PLは名前の通りMakefileを生成してくれる君で、Build.PLはModule::Buildを前提としたシステムになっている。
詳細はモダンPerlの世界へようこそ 第23回 Module::Build:MakeMakerの後継者を目指してが詳しい。
今回は読みたいコードがList::Utilで、List::UtilはMakefile.PLを使っていたのでこちらの雰囲気を見る。
最終的にMakefileを生成しにいくのはExtUtils::MakeMaker
が提供しているWriteMakefile
を実行するタイミングである。
List::UtilのMakefile.PLの場合は一番最後の行でこのように実行している。
WriteMakefile(%params);
この引数%params
にはビルドに関する様々な情報を入れる事ができる。
ExUtils::MakeMakerのドキュメントを見ると、XSの最適化の度合いを調整出来るOPTIMIZEがオプションとして存在している。
ドキュメントを見る限りCの最適化オプションをそのまま渡せるので、デバッグしたかったら-O0 -g
あたりを渡せばよいだろう。
ということでList::Utilのparamsを作っている箇所の一番下にしれっとOPTIMIZEを指定する。
my %params = (
NAME => q[List::Util],
ABSTRACT => q[Common Scalar and List utility subroutines],
AUTHOR => q[Graham Barr <gbarr@cpan.org>],
DEFINE => $defines,
DISTNAME => q[Scalar-List-Utils],
VERSION_FROM => 'lib/List/Util.pm',
OPTIMIZE => '-g -O0',
準備は整ったのでビルドしていく。まずはMakefileを生成する必要があるので、Makefile.PLを実行する。
❯ perl Makefile.PL
Checking if your kit is complete...
Looks good
Generating a Unix-style Makefile
Writing Makefile for List::Util
Writing MYMETA.yml and MYMETA.json
無事Makefileができたので素朴にmakeする
❯ make
cp lib/Scalar/Util.pm blib/lib/Scalar/Util.pm
cp lib/List/Util.pm blib/lib/List/Util.pm
cp lib/Sub/Util.pm blib/lib/Sub/Util.pm
cp neko.pl blib/lib/List/neko.pl
cp lib/List/Util/XS.pm blib/lib/List/Util/XS.pm
Running Mkbootstrap for Util ()
chmod 644 "Util.bs"
"/Users/anatofuz/.plenv/versions/5.32.0/bin/perl5.32.0" -MExtUtils::Command::MM -e 'cp_nonempty' -- Util.bs blib/arch/auto/List/Util/Util.bs 644
"/Users/anatofuz/.plenv/versions/5.32.0/bin/perl5.32.0" "/Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0/ExtUtils/xsubpp" -typemap '/Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0/ExtUtils/typemap' ListUtil.xs > ListUtil.xsc
mv ListUtil.xsc ListUtil.c
cc -c -fno-common -DPERL_DARWIN -mmacosx-version-min=11.2 -fno-strict-aliasing -pipe -fstack-protector-strong -DPERL_USE_SAFE_PUTENV -g -O0 -DVERSION=\"1.56\" -DXS_VERSION=\"1.56\" "-I/Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0/darwin-2level/CORE" -DPERL_EXT -DUSE_PPPORT_H ListUtil.c
rm -f blib/arch/auto/List/Util/Util.bundle
cc -mmacosx-version-min=11.2 -bundle -undefined dynamic_lookup -fstack-protector-strong ListUtil.o -o blib/arch/auto/List/Util/Util.bundle \
\
chmod 755 blib/arch/auto/List/Util/Util.bundle
Manifying 4 pod documents
無事makeが実行できた。
ちなみにmake clean
とするとMakefileごとビルドしたものを消してくれるので便利。
ビルドするともとのxsのコードを純粋なC言語*1に変換されたものなどが生成される。
実際にPerlインタプリタが読みこめるファイル形式のものはblib以下に書き出される。
blibディレクトリはMakeMakerがビルドしたファイルが含まれている。
List::Utilの場合は次のようなファイルが含まれている。
❯ tree blib
blib
├── arch
│ └── auto
│ └── List
│ └── Util
│ └── Util.bundle
├── bin
├── lib
│ ├── List
│ │ ├── Util
│ │ │ └── XS.pm
│ │ └── Util.pm
│ ├── Scalar
│ │ └── Util.pm
│ ├── Sub
│ │ └── Util.pm
│ └── auto
│ └── List
│ └── Util
├── man1
├── man3
│ ├── List::Util.3
│ ├── List::Util::XS.3
│ ├── Scalar::Util.3
│ └── Sub::Util.3
└── script
16 directories, 9 files
ちなみにUtil.bundleの場合はバイナリファイルだったりする。
普通のPerlのライブラリの場合はlib以下のものを@INC
に入れれば実行できた。
似た感じでblib以下のものをPerlに教えてあげれば自分でビルドしたXSをPerlインタプリタから呼び出す事ができる。
blibをPerlスクリプトから呼び出すにはblibモジュールを使う方法もあるけれど、勝手にカレントのblibディレクトリ以下を読み込んでくれるExtUtils::testlibを使うのが便利。
というわけで自分でビルドしたList::Utilを呼び出すサンプルコードは次のようになる。今回はめんどくさいのでビルドしたディレクトリ上でやっている。
use strict;
use warnings;
use ExtUtils::testlib;
use List::Util qw/all/;
my $hoge = [1,2,3];
if (all { $_ } @$hoge) {
print "hello!\n";
}
さて例題も書けたのでデバッグしていこう。
一点ポイントとなるのは、このblibを実行できるのは、XSをビルドする際に利用したPerlじゃなくと実行ができない。
今回では上のログにある通り/Users/anatofuz/.plenv/versions/5.32.0/bin/perl5.32.0
が実行したPerlのバイナリである。
これ以外のPerlのバイナリ、例えば/usr/bin/perl
で実行すると以下のような感じでモジュールロードに失敗する。
❯ /usr/bin/perl neko.pl
Can't load '/Users/anatofuz/src/github.com/Dual-Life/Scalar-List-Utils/blib/arch/auto/List/Util/Util.bundle' for module List::Util: dlopen(/Users/anatofuz/src/github.com/Dual-Life/Scalar-List-Utils/blib/arch/auto/List/Util/Util.bundle, 0x0001): symbol '_PL_DBsub' not found, expected in flat namespace by '/Users/anatofuz/src/github.com/Dual-Life/Scalar-List-Utils/blib/arch/auto/List/Util/Util.bundle' at /System/Library/Perl/5.30/darwin-thread-multi-2level/DynaLoader.pm line 197.
at neko.pl line 4.
Compilation failed in require at neko.pl line 4.
BEGIN failed--compilation aborted at neko.pl line 4.
ビルド時に使ったPerlだと問題なく実行できる。
❯ /Users/anatofuz/.plenv/versions/5.32.0/bin/perl5.32.0 neko.pl
hello!
ではデバッガをかけて実行してみる。今回はlldbでやる。
❯ lldb -- /Users/anatofuz/.plenv/versions/5.32.0/bin/perl neko.pl
(lldb) target create "/Users/anatofuz/.plenv/versions/5.32.0/bin/perl"
Current executable set to '/Users/anatofuz/.plenv/versions/5.32.0/bin/perl' (arm64).
(lldb) settings set -- target.run-args "neko.pl"
(lldb)
今回デバッグしたいxsのファイルはListUtil.xs
で、使っているallのコードを見てみたい。
allは実はanyのエイリアスで、any関数は702行目から本体がある。
というわけでListUtil.xsの702行目にbreak pointを貼ってみる。
(lldb) b ListUtil.xs:702
Breakpoint 1: no locations (pending).
WARNING: Unable to resolve breakpoint to any actual locations.
そして実行すると見事に止まる。
(lldb) process launch
Process 69006 launched: '/Users/anatofuz/.plenv/versions/5.32.0/bin/perl' (arm64)
1 location added to breakpoint 1
Process 69006 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00000001004ae5a0 Util.bundle`XS_List__Util_any(cv=0x00000001010fd810) at ListUtil.xs:702:22
699 PROTOTYPE: &@
700 PPCODE:
701 {
-> 702 int ret_true = !(ix & 2); /* return true at end of loop for none/all; false for any/notall */
703 int invert = (ix & 1); /* invert block test for all/notall */
704 GV *gv;
705 HV *stash;
Target 0: (perl) stopped.
これで止まるとこっちのものなので、あとはnextしたり値をprintしたりして確かめていくと普通に読んでいける。
読み方としてはPerlインタプリタ読んでるのと同じなので、SVの中身に思いを馳せる感じになっていくと思う。
というわけでXSも実はlldbで読める!!! 読んでいくぞ!!!