libf2c のバグ

English page

FORTRAN77 を C/C++(ただしまるで機械語、可読性は皆無)に変換する f2c は、役に立つものではあるが、いくらかバグがあるようです。

以下は、この文章を書いている2016/09/09時点でNetlibにある libf2c.zip (含まれるファイルのうち最新のもののタイムスタンプが2013/09/26のもの)で見つけたバグです。


rsli.c READ文で読む文字列に128以上の文字コードが含まれるときに生じるバグの修正

修正箇所
16-42
修正前
static int i_getc(Void)
{
	if(f__recpos >= f__svic->icirlen) {
		if (f__recpos++ == f__svic->icirlen)
			return '\n';
		z_rnew();
		}
	f__recpos++;
	if(f__icptr >= f__icend)
		return EOF;
	return(*f__icptr++);
	}

 static
#ifdef KR_headers
int i_ungetc(ch, f) int ch; FILE *f;
#else
int i_ungetc(int ch, FILE *f)
#endif
{
	if (--f__recpos == f__svic->icirlen)
		return '\n';
	if (f__recpos < -1)
		err(f__svic->icierr,110,"recend");
	/* *--icptr == ch, and icptr may point to read-only memory */
	return *--f__icptr /* = ch */;
	}
修正後
static int i_getc(Void)
{
	if(f__recpos >= f__svic->icirlen) {
		if (f__recpos++ == f__svic->icirlen)
			return '\n';
		z_rnew();
		}
	f__recpos++;
	if(f__icptr >= f__icend)
		return EOF;
	return((unsigned char)*f__icptr++); /* fix for 8bit char */
	}

 static
#ifdef KR_headers
int i_ungetc(ch, f) int ch; FILE *f;
#else
int i_ungetc(int ch, FILE *f)
#endif
{
	if (--f__recpos == f__svic->icirlen)
		return '\n';
	if (f__recpos < -1)
		err(f__svic->icierr,110,"recend");
	/* *--icptr == ch, and icptr may point to read-only memory */
	return (unsigned char)*--f__icptr /* = ch */;  /* fix for 8bit char */
	}

元は extern char *f__icptr; と宣言されているので、これのポインタが示す要素値を暗黙の型変換でint型に変換しようとすると、元の入力が0x80以上の文字コードの場合に負の数を返すことになってしまう。その結果、処理系にもよるが、自分で書いているプログラムに間違いがないにもかかわらず不可解な挙動を示す。
たとえばMS-VC++では、リリースモードでは訳のわからない挙動を示す上に、不具合個所が追跡ができない。プログラム全体に libf2c 自身のソースコードを含めてデバッグモードでビルドすると、この i_getc() の後段にくる isdigit() 等でASSERTが生じて、初めて原因がわかった。
宣言自体をextern unsigned char *f__icptr; に変更するのは当たりが大きいので、この場所だけ絆創膏を貼る
なお、iio.c には return(*(unsigned char *)f__icptr++); という表記があり、これに関して i77vers.c に以下の言及があるので、単純に rsli.c が修正漏れだったようだ。

/* 24 Feb. 1995: iio.c: z_getc: insert (unsigned char *) to allow
		 internal reading of characters with high-bit set
		 (on machines that sign-extend characters). */

lread.c 配列添字オーバーランの修正

修正箇所
54-64
修正前
char f__ltab[128+1] = {	/* offset one for EOF */
	0,
	0,0,AX,0,0,0,0,0,0,WH|B,SX|WH,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	SX|B|WH,0,AX,0,0,0,0,AX,0,0,0,SG,SX,SG,0,SX,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,EX,EX,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	AX,0,0,0,EX,EX,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
修正後
char f__ltab[256+1] = {	/* offset one for EOF */ /* changed from 128 to 256 */
	0,
	0,0,AX,0,0,0,0,0,0,WH|B,SX|WH,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	SX|B|WH,0,AX,0,0,0,0,AX,0,0,0,SG,SX,SG,0,SX,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,EX,EX,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	AX,0,0,0,EX,EX,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};

rsli.c の上記修正に伴い、マクロ GETC(ch) が128以上255以下の値も正しく返すことが可能になり、その結果として、文字種別テーブル f__ltab[] が元のままでは配列添字オーバーランしてしまう。
ちなみに、rsli.c の修正前は添字のマイナス側にオーバーランする可能性がある。ただし実際はその手前の別処理段階で排除される場合もあるようだ。まだ挙動をトレースしきれていない。


lread.c メモリリーク

オリジナルの lread.c の 501 493行目で、広域変数として宣言されているポインタ f__lchar に確保したメモリのアドレスを入れていますが、これはその前に何か残っていた場合は開放するが、確保した後いつまで有効かは確保時点で決めることができない(理由は、FORTRANのREAD文において、要素が足りない場合に次の行を食べて要素を探し出そうとする言語仕様があるため)。よって、プログラムの中で最後に読み込んだ文字列に関するバッファが、プログラム終了時点で必ずメモリリークします。これを修正する手段はどうやら無さそうです。些少なので無視するしかないでしょう。

このメモリリークは、MS-VC++ を用いて以下のようにして検出(証明)できる。他のコンパイラではできません。

以下の5行を libf2c ソースツリー中の f2c.h の先頭に追加する。

#ifdef _MSC_VER
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#endif

以下の4行を libf2c ソースに含まれる main.c の先頭に追加する。

#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#include <string.h>
#include <windows.h>

libf2c ソースに含まれる main.c の中で main() を定義しているところで

 int
#ifdef KR_headers
main(argc, argv) int argc; char **argv;
#else
main(int argc, char **argv)
#endif
{
xargc = argc;
xargv = argv;
 int
#ifdef KR_headers
main(argc, argv) int argc; char **argv;
#else
main(int argc, char **argv)
#endif
{
   _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

	_set_abort_behavior(0, _CALL_REPORTFAULT); // ワトソン クラッシュ ダンプを抑止
	_set_abort_behavior(1, _WRITE_ABORT_MSG); // メッセージは表示する、ただしdebug mode only

 	
   // レポートをファイルに書き出させる。
   _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
   _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE);
   _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);

   // レポートを標準エラー出力に出力させる。
   _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
   _CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR);
   _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
xargc = argc;
xargv = argv;
にする。

これらを用いて libf2c を MS-VC++ のデバッグモードでビルドしておく(f2c の tips / libf2c の MS-VC++ によるビルド を参照し、makefile.vc の修正箇所で /MD を /MDd にする)。その成果物をここでは vcf2c32dbgmem.lib と仮定する。

前述の改造した f2c.h を、f2cで変換したCソースと同じディレクトリにおいておく。デバッグモードでコンパイルする。さらに上記デバッグモードでビルドした libf2c をリンクする。以下は一例であり、Cとlibのファイル名は状況に応じて変更。

cl /MDd foobar.c vcf2c32dbgmem.lib /link /SUBSYSTEM:CONSOLE
デバッグ出力の行番号は正しくない(Cの既定義マクロ __LINE__ が操作されてしまうらしい)が、メモリリークをとらえることができる。以下は一例。
Detected memory leaks!
Dumping objects ->
open.c(58) : {91} normal block at 0x00702868, 4096 bytes long.
 Data: < ]              > 20 5D 0A 00 20 20 20 20 20 20 20 20 20 20 20 20
lread.c(501) : {87} normal block at 0x006FD7D0, 128 bytes long.
 Data: <0.2             > 30 2E 32 00 CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.


signal_.c の LLP64 メモリーモデルとの不整合(対策不能?)

これは殆どの場合、実害は生じない。実害があるのは、FORTRAN77 のソースコード中で signal を呼んでさらにその戻り値を利用しようとした場合である。

不具合箇所

修正前
#include "f2c.h"
#include "signal1.h"
#ifdef __cplusplus
extern "C" {
#endif

 ftnint
#ifdef KR_headers
signal_(sigp, proc) integer *sigp; sig_pf proc;
#else
signal_(integer *sigp, sig_pf proc)
#endif
{
	int sig;
	sig = (int)*sigp;

	return (ftnint)signal(sig, proc);
	}
#ifdef __cplusplus
}
#endif
修正後 (N/A)

C言語の signal 関数の戻り値はシグナルハンドラ(関数へのポインタ)または定数マクロであり、前者は 64bit アーキテクチャーなら 64bit の値になる。しかし f2c.h における typedef long int ftnint; は LP64 では 64bit なので問題ないが、LLP64 では 32bit になるため signal 関数の戻り値を受け止めきれず不正な型キャストが発生してしまう。具体的には、MS-VC++ 2017 Express では警告レベル無指定(/W2 相当)で「signal_.c(17): warning C4311: '型キャスト': ポインターを '_crt_signal_t' から 'ftnint' へ切り詰めます。」となる。意味不明な警告表示ではあるが、LLP64 において誤実装であることには変わりない。

なお、今風の C++11/C99 処理系なら戻り値の型を intptr_t で宣言することになるだろうが、これだけでは不十分。f2c の変換翻訳の実装もおかしいようだ。

よって f2c 自身も改造しないと正しい扱いはできない。本件は事実上修正不能であり、放置するしかない。


このほか、f2c の tips - CYCLE文・EXIT文に対応させるプリプロセッサと len_trim 関数の代替品 も参照してください。


略歴


トップページ / 計算機関係の雑多なこと / libf2c のバグ
このサイトについて・著者連絡先