LoginSignup
2
0

More than 5 years have passed since last update.

Perlの-Eオプションとfeatureプラグマに潜む罠

Last updated at Posted at 2019-05-04

環境

$ perl -v
This is perl 5, version 28, subversion 1 (v5.28.1) built for x86_64-linux-gnu-thread-multi
$ apt show perl
Package: perl
Version: 5.28.1-6

はじめに

CTFをやっていて二つの文字列のXORをちょくちょく確認したくなった。
横着者なのでこういうときは大抵perlでワンライナーしてさっと確認するのですが、

$ perl -E 'say "reiwa" ^ "SHOWA"'
0

動かぬ。なんで?(殺意)

検証

わかりずらいので片方の文字列を"Perl"、もう片方の文字列を"c*(^"とします。(良い例が思いつかなかったので結局分かり辛い)
"Perl"と"c*(^"のXORは"30Z2"になります。

$ perl -E 'say "c*(^" ^ "Perl"'
0 #本来なら30Z2

とりあえず、XORがワンライナーで動作するか確認します。

$ perl -E 'printf "%b", 0b0101011101 ^ 0b01101110'
100110011

動いた。文字で試してみる。

perl -E 'printf "%b\n", "Perl" ^ "c*(^"'
0

今度は動かない。文字周りに問題があるのだろうか。

次に、プログラム自体にミスが無いかファイルに起こして実行してみました。

$ cat test.pl && echo "----result----" && perl test.pl
use 5.28.1;

say "c*(^" ^ "Perl";
print "c*(^" ^ "Perl","\n";
----result----
0
0

sayでもprintでも動きませんでした。この0は何なんでしょう。
バージョン周りを確認します。バージョンを5.28.1から5.26.1に戻します。

$ cat test.pl && echo "----result----" && perl test.pl
use 5.26.1;

say "c*(^" ^ "Perl";
print "c*(^" ^ "Perl","\n";
----result----
3OZ2
3OZ2

動いた。つまり、不具合は記法等に関係なくバージョンが関係していることが分かった。
つまり、最初のワンライナーも次のようにすれば動く。

$ perl -e 'print "Perl" ^ "c*(^","\n"'
3OZ2

同じprintでも-Eオプションだと動かない。

$ perl -E 'print "Perl" ^ "c*(^","\n"'
0

考察

要は、ver5.28.1に原因があり、-Eオプションを利用することで5.28.1の不具合が露見するということです。
しかし、そうなると様々な疑問が生じます。
・何故、-eオプションでは問題なく動作するのか?
・5.28.1で不具合が生じるのはXORだけなのか?
・仕様なのか?バグなのか?

何故、-eオプションでは問題なく動作するのか?

そもそも、-eと-Eの違いは何でしょう。
僕の中では-Eの方がsayが使えて便利程度の感覚だったのですが。

$ perl -e 'print $^V , "\n"'
v5.28.1
$ perl -E 'say $^V'
v5.28.1

見た感じ適応されるバージョンは一緒みたいです。
じゃあ何が違うのでしょうか。

-E commandline
behaves just like -e, except that it implicitly enables all optional features (in the main compilation unit). See feature.
perlrun - perldoc.perl.org

Perl公式のリファレンスによると、-Eオプションは最新バージョンを指定するとかでは無くてfeatureプラグマ(新しいシンタックス[say,switch,state,...])をまとめて有効にするオプションだそうです。うーん。
featureについては公式リファレンスのfeatureの項目をどうぞ。

解決

じゃあ、-eオプションで実行した場合は5.28.1からfeatureの機能を抜いた機能で実行されるのでしょうか。それとも、もっと前のバージョンで実行されているのでしょうか。
-eオプションで実行するのはPerlでオプションをつけずにuse version;でバージョン宣言しない状態と同じです。

なんて、いろいろ考えてfeatureのリファレンスを眺めていたら あ り ま し た 。

The 'bitwise' feature

WARNING: This feature is still experimental and the implementation may change in future versions of Perl. For this reason, Perl will warn when you use the feature, unless you have explicitly disabled the warning:

   no warnings "experimental::bitwise";

This makes the four standard bitwise operators (& | ^ ~ ) treat their operands consistently as numbers, and introduces four new dotted operators (&. |. ^. ~. ) that treat their operands consistently as strings. The same applies to the assignment variants (&= |= ^= &.= |.= ^.= ).

See Bitwise String Operators in perlop for details.

This feature is available from Perl 5.22 onwards.
feature - perldoc.perl.org

どうやら、feature内では文字形式のビット演算子と数値形式のビット演算子は別の表現になるようです。

演算子 数値 文字
and & &.
or | |.
xor ^ ^.
not ~ ~.

つまり、こうすれば-Eオプションでも動くということです。

$ perl -E 'print "Perl" ^. "c*(^","\n"'
3OZ2

一件落着。(満身創痍)
なので、残りの疑問にも答えが出ます。

ver5.28.1で不具合が生じるのはXORだけなのか?

上で述べた通り、XORだけでなくビット演算に関係するAND,OR,NOTに関しても同様の現象が生じることになります。

仕様なのか?バグなのか?

バグなんてとんでもない。ちゃんと発表されている仕様です。
それを不具合なんて...開発者のひとごめんなさい。
これからはちゃんとアップデートの度に仕様書読むようにするので許してください。

じゃあver5.26.1では動いてver5.28.1では動かないのは何故なのか

use 5.26.1use 5.28.1でXORの挙動が違ったのは何ででしょうか。
どちらもfeatureを宣言していないのは明らかです。
これも、ver5.28の公式ドキュメントを調べてみるとこうありました。

String- and number-specific bitwise ops are no longer experimental

The new string-specific (&. |. ^. ~.) and number-specific (& | ^ ~) bitwise operators introduced in Perl 5.22 that are available within the scope of use feature 'bitwise' are no longer experimental. Because the number-specific ops are spelled the same way as the existing operators that choose their behaviour based on their operands, these operators must still be enabled via the "bitwise" feature, in either of these two ways:

use feature "bitwise";
use v5.28; # "bitwise" now included

They are also now enabled by the -E command-line switch.

The "bitwise" feature no longer emits a warning. Existing code that disables the "experimental::bitwise" warning category that the feature previously used will continue to work.

One caveat that module authors ought to be aware of is that the numeric operators now pass a fifth TRUE argument to overload methods. Any methods that check the number of operands may croak if they do not expect so many. XS authors in particular should be aware that this:

SV *
bitop_handler (lobj, robj, swap)

may need to be changed to this:

SV *
bitop_handler (lobj, robj, swap, ...)

perldelta - what is new for perl v5.28.0 - Perldoc Browser

どうやら、ver5.28以降はfeatureを宣言しなくてもデフォルトで文字形式のビット演算子と数値形式のビット演算子を別の表現として扱うようです。(絶望)
つまり、これも

bad.pl
use 5.28.1;

say "c*(^" ^ "Perl";
print "c*(^" ^ "Perl","\n";

こうすれば動くということです。

good.pl
use 5.28.1;

say "c*(^" ^. "Perl";
print "c*(^" ^. "Perl","\n";

これで全ての謎が解けました。

結論

まず、最初に掲げた

$ perl -E 'say "reiwa" ^ "SHOWA"'
0

は、

$ perl -E 'say "reiwa" ^. "SHOWA"'
!-&  

にすることで正しく表示されることがわかりました。
原因は、-Eオプションにはfeatureプラグマを有効にする効果がありfeature内では文字形式のビット演算子と数値形式のビット演算子は別の表現として扱うため、数値形式のXORである^演算子を使った場合正しい結果が得られないことだと分かりました。

つまり、明示的に5.28以上のバージョンを使用するまたは、featureを有効にして実行する時は、ビット演算の際に以下の記号を使い分ける必要があります。

演算子 数値 文字
and & &.
or | |.
xor ^ ^.
not ~ ~.

教訓

そもそも今回のトラブルはちゃんと公式のドキュメントで言語仕様を確認していなかったことに原因があります。また、-Eオプションの仕様や意味を理解しないまま使っていたことも深みにハマってしまった原因です。
今回得られた教訓は以下の二つです。

・Perlの-Eオプションはfeatureプラグマをまとめて有効にするので自分が使いたい機能だけでなくfeatureプラグマで有効になる全ての機能の仕様や動作を確認する必要がある。
・言語のバージョンアップは他人事ではなくて実際にこうやって自分がダメージを負うこともある。仕事として使って無かろうがバージョンアップの度に公式ドキュメントを見て変更点を確認しないと痛い目をみる。

あと、-Eオプションって矛盾じみた表現で正しくは-EまたはEオプションと表現するのが正しいですが、-Eオプションの方が伝わりやすいと判断してこっちにしました。許して(懇願)。

参考

perlrun - perldoc.perl.org
feature - perldoc.perl.org
Perl の文法上の新機能が使える feature プラグマ詳解 - Qiita

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0