とある16bit組込み開発環境
sprintf関数は低メモリ消費版ライブラリ使用
問題のコード
問題のあった状況を抽出すると以下のようなコードになる。
# include <stdio.h>
# include <stdbool.h>
typedef struct {
float pi;
bool yesno;
} MY_DATA;
int main(void) {
MY_DATA adata;
adata.pi = 0.0;
adata.yesno = false;
printf("1:%d\n", adata.yesno);
printf("2:%Ld\n", adata.yesno);
adata.pi = 3.141592;
adata.yesno = false;
printf("3:%d\n", adata.yesno);
printf("4:%Ld\n", adata.yesno);
return 0;
}
上記において、4番の値が0でない。
(例: 655360)
問題発生条件
以下の条件が全て成立する時だけ発生する。
- 某開発環境のprintf関数使用時
- gccなどにあるような一般的なprintf関数ではない
- printf書式指定子の指定間違い
- "%d"とするところを"%Ld"としていた
- 構造体内の隣の変数piの値が0以外の時
起きたことの推測
構造体変数のメモリ配置は以下に例がある。
https://stackoverflow.com/questions/20737176/how-are-struct-members-allocated-in-memory
上記の構造体ではpiの後にyesnoが続いている。
| pi | yesno |
yesnoの値を問題のprintf()で"%Ld"として出力しようとした時に、piの領域も含めて読んだ値が出力され、655360のような値が出てきたように思われる。
"%d"に修正したところ、0が出るようになった。
予防措置
- printf()の書式指定子間違いがないことを確認する
- デバッグ時にyesno以外のメンバ変数にダミー値を入れて、yesno値に影響がないことを確認する
- 変数全てに関して、同じ確認を行う
- データ測定時の動作検証をする
「データ測定」がいつでも可能という状況でない場合、措置1,2できちんと対処しておくことで、措置3の検証時間を抑える。
補足
以下のprintfでは発生しない。
- ideone上のC (gcc version 6.3)
- gcc (version 4.4.7) on CentOS 6.8
書式指定子間違いがあっても、隣の値を読まないよう処理がきちんと実装されているのかもしれない。
書式指定子の指定間違いをきちんと警告として出す開発環境ならこういうミスは減るだろう。
例として、@fujitanozomu さんに教えていただいた方法。
https://ideone.com/RLp2fr
compilation info
prog.c: In function ‘main’:
prog.c:121:14: error: format ‘%Ld’ expects argument of type ‘long long int’, but argument 2 has type ‘int’ [-Werror=format=]
printf("2:%Ld\n", adata.yesno);
^
prog.c:127:14: error: format ‘%Ld’ expects argument of type ‘long long int’, but argument 2 has type ‘int’ [-Werror=format=]
printf("4:%Ld\n", adata.yesno);
^
cc1: some warnings being treated as errors
訂正
(追記 2017/09/20)
printf()でなくsprintf()を用いたchar 配列への代入で起きていた。
ideoneでsprintf()を試したが、同じ症状にはならない。