こんにちは。ディマージシェアの技術担当です。今回はプログラミングをする際、ソースコード上で扱う「文字列」に関して少し掘り下げてみます。
プログラミング言語における文字列の扱いは、最近の言語ではコーディングに都合が良いように上手く言語設計がされています。JavaScriptやPHPなどは文字列をあたかも数値型のように扱うことができます。Javaでは文字列であることを少し意識していないと初歩的なバグを埋め込んでしまうことがあります。C言語あたりになると、文字列は型としては定義すらされていません。
これらの違いは、1つの言語を使い続けているうちは大きなトラブルになりませんが、「便利に実装された言語 => 原始的な言語」の方向に使う言語が変わったとき、正しい知識を持っていないとトラブルが発生することがあります。
本記事ではいくつかのプログラミング言語における文字列の扱いと、言語ごとの扱いの違いにまつわるトラブルを紹介したいと思います。
文字列の定義はプログラミング言語内での取り決めに過ぎない
例えばC言語における文字列は次のような取り決めです。
与えられたアドレスを起点に\0以外の文字が連続するバイト列
\0の出現をもって終端とみなす
※「\0」は「null文字」を表すときによく使われる記法です
なので、"abc\0"というような並びでメモリ上に配置され、変数の値としてはaの場所(アドレス)という扱いになります。型は、メモリ上のアドレスなので一般の計算機ではCPUのレジスタ長の整数(ptr_t)になります。C言語の世界では型としての文字列型は定義されておらず、整数値で始まる場所を伝えたら、そこを起点に\0が出現するまでを文字列として扱います。
Javaではどうでしょうか。おおまかに次のようになっています。
文字列開始位置、文字列長をペアで型として管理
型の値としてはメモリ上の位置
という感じです。C言語との違いは、"abc\0def"のように、文字列内にnull文字が許容されるようになり、終端の判定は文字列の長さを型に内包する形で実現しています。この構造がString型という文字列専用の型として用意されています。
PHPでは更に便利になり、
文字列開始位置、文字列長、その他メタ情報を型として管理
型の値としては文字列
という感じになっています。Javaとの違いはこれだけではわかりにくいですが、
<?php
$a = "abc";
$b = "abc";
if ($a == $b) {
}
のように演算子で直接比較ができます。Javaでは
String a = "abc";
String b = "abc";
if (a.equals(b)) {
}
と書く必要があり、Javaで文字列型同士を==で比較すると不具合の原因になります。Javaの場合、文字列型同士を==で比較した場合は、「文字列の場所が同じかどうか」という処理になり、コンパイルエラーにはならず、実行時のバグとして開発者を悩ませることになります。
よくあるトラブルと解決するための知識
Javaあるある
先ほど、Javaでは
String a = "abc";
String b = "abc";
if (a == b) {
}
のようにコーディングするとバグの原因となる旨を書きました。ところが。上記のソースはa == bの結果trueになり、書いた人の思った通りの結果になってしまいます。ちょっと知識のある開発者ならば、a == bがfalseになってほしいところです。このコードを、
String a = new String("abc");
String b = new String("abc");
if (a == b) {
}
と直すと、a == bがfalseになり、予定通りバグるはずです。
Javaでは、文字列リテラルをソースコード内で使いまわすと文字列リテラルの位置を使いまわして変数に格納するため、コード内で"abc"と2か所に書いても変数には同じ値が入るようにコンパイルされます。なので、文字列リテラルを直に変数に代入する書き方だと、予期しない予想通りの挙動、という難解な挙動に悩まされる可能性があります。多少冗長ではありますが、new String("abc"); の書き方を積極的に選んだ方が、不思議なトラブルに遭遇しにくくなります。
処理系をまたぐ文字列に注意
例えばPHPやJavaの文字列をC言語で作られた処理系に渡す際、null文字に十分に注意する必要があります。nullバイトインジェクションという有名な攻撃もあります。次のコード片はphp.netから引用しました。
<?php
$file = $_GET['file']; // ここで "../../etc/passwd\0" が渡されたとします
if (file_exists('/home/wwwrun/'.$file.'.php')) {
// file_exists は true を返します。これは、ファイル /home/wwwrun/../../etc/passwd が存在するからです
include '/home/wwwrun/'.$file.'.php';
// ファイル /etc/passwd がインクルードされてしまいます
}
?>
nullバイトを含む文字列をC言語で作られた処理系にそのまま渡してしまうと意図しない動作が起こることがあります。
まとめ
今回は、いくつかのプログラミング言語における文字列の扱いと、それにまつわるトラブルを紹介しました。文字列に関する知識は正しく保有していないと、単なるバグを引き起こすだけでなく、場合によっては致命的な脆弱性を埋め込んでしまう可能性があります。
ソースコードを書くエンジニアが複数のプログラミング言語それぞれに精通するのはとてもコストがかかるので、言語を横断したときにトラブルになりうるポイントをピックアップしてまとめてみると面白いかもしれません。