chikuchikugonzalezの雑記帳

趣味とか日記とかメモとか(∩゚д゚)

(s)printfフォーマット指定子について

C/C++カテゴリとMUGENカテゴリが同じ記事につくとは1年前は思いもしなかったが、MUGENのデバッグ出力がsprintfフォーマットなんだからしかたあるまい。
で以前MUGENのsprintfフォーマットを調べたわけですが (RC6でDisplayToClipboardに使えないフォーマットがあった - chikuchikugonzalezの雑記とかDisplayToClipboardで使用可能な書式 - chikuchikugonzalezの雑記とか)、ここ最近とある業界で%n系のバグが見つかったようでにわかに活気づいております。
で、この「%n」ってなによ?ってことでちょいと調べました。
ところで%nってDisplayToClipboardの変換指定子のことであってますよね?
違ったら以下の文章はすべて駄文になりまっせ( TДT)

%n指定子って?

分かりづらく言うと、

(s)printfでフォーマット文字列内に'%n'が指定された場合、そこまで出力された文字列の文字数が%nに対応する位置のポインタ変数の指す整数型変数に格納される

ということ。わからん。

サンプルで示すと、だいたい次のC言語プログラム

#include <stdio.h>
#include <stdint.h>
// percentn1.c
int main(int argc, char *argv[]) {
	int i = 0;
	int c = 0;

	for (i = 0; i < argc; i++) {
		printf("%s%n", argv[i], &c);    // %nには変数へのポインタを渡す
		printf(" (%d文字)\n", c);       // ポインタが指していた変数に文字数が格納される
	}

	return 0;
}

を次のように実行すると

$ percentn1 "Hello, World" "Sample Code"

次のようになります

percentn1 (9文字)
Hello, World (12文字)
Sample Code (11文字)

おわかりだろうか。出力をするはずのprintfで入力系の変換指定子になっているのである。

これ一歩間違えるとこんなこともできまっせ

まぁ、int型をポインタと誤認させても行けるってことです。

#include <stdio.h>
#include <stdint.h>

int main(void) {
	int i = 0;
	int j = (int) &i;		// ポインタをintにしてみる

	printf("Hello, World!%n", j);		// ポインタの値を持った「int型」変数を指定
	printf(" (%d文字)\n", i);			// ポインタの指していた場所を表示

	return 0;
}

結果

$ percentn2
Hello, World! (13文字)

MUGENでやると?

こんな感じですか

; Gルガールのデバッグ部分
; %n指定子にvar(49)を指定してみた
[State -2:          Debug Flags]
type     = AppendToClipboard
trigger1 = !IsHelper
text     = "\nCHAIN:%d SC:%d ULT:0x%08X KILL:%d%n"
params   = var(10), var(11), var(51), var(57), var(49)

これってつまり

var(49)に入っている値をポインタとして使い、そこに出力文字数を格納する

という動作をします。ぶっちゃけメモリ上の任意の位置に書き込みできます。
また書きこむ値も結構自由です。%10dとすれば最低10文字出力されることになるので、%255dとかすれば255がvar(49)の指すメモリに書きこまれます。
まー普通はそんなところへアクセスすればあっさりSEGV (セグメンテーション違反、別名アクセス違反) でプログラムが死にます *1

Win版だけ?

どうやらelecbyteもこのあたりのバグを知っていた様子で、実行前にフォーマット指定子をチェックしてくるようになりました。
そのなかで使えないのは

  • %x
  • %o
  • %c
  • %n ← New!

となります。使っていると対戦画面に行く前に「ダイアログで」(゚Д゚)ゴルァ!!って怒ってきます。逆に言えば安全にそのまま終了します。

*1:昔のOSだったらOSごと死んでたんだろうなー