Hatena::ブログ(Diary)

七誌の開発日記(旧) このページをアンテナに追加 RSSフィード

新ブログ Twitter OneDrive Wiki

2011-11-10

[][]手動呼び出し

COMのメモリレイアウトを理解するため、なるべく既存のマクロを使わないでC言語から呼び出してみました。

手軽にCOMのサンプルを作る方法が見当たらなかったので、以下の Project1.dll を使わせていただきました。

gistに掲載したサンプルは Project1.dll を直接読み込むためregsvr32による登録は不要です(regsrv32とtypoしてハマりました)。.NETになってからはCOMにtlbを公開する(CCW)のにmscoree.dllを経由するためregasmが必須になりました。VB6のように独立したCOM DLLを手軽に作る手段はないようです。

参考サイト

2011-02-20

[]引数戻り値の省略

PE勉強会(1)で配布した資料(id:n7shi:20110219)のサンプルコードに問題があり、ご指摘を受けました。調べた所、色々な意味で突っ込みどころが満載でした。

int _main() {}

結論から言うと、以下のように書くのが無難なようです。

int _main(void) { return 0; }

ただのmainではなく _ が付いていますが、その理由については後述します。

1. mainのreturn省略

まず _ を付加しない普通のmainについてです。C++とC99ではmainでreturnを省略すると自動的に0を返すコードが付加されます。詳細はid:syohexさんがまとめてくださいました。

念のため再確認してみます。以下のコードを main.c として保存します。

int main() {}

これをデフォルトANSI Cをベースに独自拡張)、ANSI C、C89、C99、C++コンパイルしてアセンブリを比較します。

$ gcc -Wall -S -masm=intel -o main-gcc.s main.c
$ gcc -Wall -S -masm=intel -o main-ansi.s -ansi main.c
$ gcc -Wall -S -masm=intel -o main-c89.s -std=c89 main.c
$ gcc -Wall -S -masm=intel -o main-c99.s -std=c99 main.c
$ g++ -Wall -S -masm=intel -o main-g++.s main.c

結果は以下の通りです。

指定警告mov eax, 0
デフォルトありなし
ANSI Cありなし
C89ありなし
C99なしあり
C++なしあり

C99とC++では言語仕様でmainのreturnを省略すると0になると定義されているため、警告なしで0を返すコードが埋め込まれます。それ以外では不定値となり警告されます。

2. 非mainのreturn省略

冒頭のコードでは _ を付けて_mainとしています。これは名前をmainに似せただけの普通の関数です。説明の都合上CRTを外したかったため、特別扱いされるmainを避けて普通の関数で書いたためです。

先ほどと同様の検証を行います。以下を_main.cとして保存します。

int _main() {}

アセンブリを出力して比較します。

$ gcc -Wall -S -masm=intel -o _main-gcc.s _main.c
$ gcc -Wall -S -masm=intel -o _main-ansi.s -ansi _main.c
$ gcc -Wall -S -masm=intel -o _main-c89.s -std=c89 _main.c
$ gcc -Wall -S -masm=intel -o _main-c99.s -std=c99 _main.c
$ g++ -Wall -S -masm=intel -o _main-g++.s _main.c

すべてのパターンで警告され、自動的に0を埋め込むこともなく、不定値が返されます。C99とC++ではmainだけが特別扱いだということが分かります。

戻り型の指定

昔のC言語ではそもそも関数定義で戻り値の型指定が必要なく、値を返すかどうかは任意だったこととも関係があるようです。

最近、別件でLions本の勉強会に参加していますが、確かに当時のpre K&Rでは戻り値の型指定はありません。

/*
 * Test if the current user is the
 * super user.
 */
suser()
{

    if(u.u_uid == 0)
        return(1);
    u.u_error = EPERM;
    return(0);
}

一見、最近の型推論のある言語みたいですが、型チェックそのものがないだけです。

追記

この記事を公開した後、別のご指摘を受けました。pre K&Rまで遡らなくても、今でも戻り型を省略するとint扱いになるとのことです。

3. voidなエントリーポイント

実はこの辺の面倒を避けて、CRTなしのエントリーポイントをvoidにしていました。

void _main() {}

ですがこれ、実は何の解決にもなっていません。出力されるアセンブリコードは以下と完全に同一です。

int _main() {}

つまり実質的に不定値が返されます。通常はvoidから戻ったときのEAXは単純に無視されるため、これが問題になることはありません。

なぜ今回は問題になったかというと、WindowsではエントリーポイントからretしたときのEAXの値がプロセス戻り値として処理されるためです。

このことを確認してみます。以下のコードをa.sとして保存します。

.intel_syntax
mov eax, 123
ret

アセンブルしてEXEを出力して、戻り値を確認します。

$ gcc -nostdlib a.s
$ ./a.exe
$ echo $?
123

このためエントリーポイントは必ずintにして、明示的に値を返すのが無難だという結論になりました。

実は勉強会で $? をど忘れしていたため、現地で直接確認することができませんでした。これも反省点の一つです。

4. voidな引数

しかし問題は戻り値だけではありません。引数にも突っ込みが来ました。C言語では()と(void)は区別されます。

void test1() {}
void test2() { test1(1); }

このコードは、C言語ではどの規格を指定しても警告なくコンパイルが通ります。C++ではエラーになります。これはC言語では()は可変長扱い【訂正】引数の型指定なしと扱われるため何を渡しても素通りするのに対して、C++では(void)扱いされるため引数を入れるとエラーになるためです。

そのためC言語では引数の型チェック漏れを防ぐため、明示的に(void)と書くことは必須です。恥ずかしながらこのことはまったく知りませんでした。C++ではエラーになることは知っていたので、当然C言語でも同じだろうと勘違いしていました。

追記

この記事を公開した後、ご指摘を受けました。当初は可変長扱いと記述していたのを修正しました。

最後に

アセンブリを見るのが主眼だったので、正直、言語仕様のことは深く考えないで、コンパイルが通ればOKくらいに軽く考えていました。しかし単純なサンプルだからこそ逆に目立つため、最低限、突っ込みを受けない程度の配慮はしておくべきだと思いました。

2010-12-12

[][]FFI

Haskellをどこから使い始めれば良いのか分からずに放置していたのですが、何気なく検索したFFIが気になったので遊んでみました。

ghc

今までghciでクイックソートをやって遊んだりしただけで、ghcでコンパイルしたことがありませんでした。今回はWindows版ghcをInterixから使います。パスを通すのではなく、/usr/local/binにシンボリックリンクを作りました。

% ln -s /dev/fs/C/ghc/ghc-7.0.1/bin/ghc.exe /usr/local/bin/ghc

ハローワールド

FFIを使わない簡単なプログラムをコンパイルしてみます。

test1.hs
main = do putStrLn "hello"

ghcにソースを渡すと、同じ名前のEXE(この場合はtest1.exe)が出てきます。この辺の感覚はcscに似ています。

% ghc test1.hs
[1 of 1] Compiling Main             ( test1.hs, test1.o )
Linking test1.exe ...
% ./test1.exe
hello

FFI(スタティックリンク)

シンプルな例を見つけたので、試してみました。

上の例ではghciから呼び出していますが、C言語とHaskellのコードを同時にコンパイルして、1つのEXEに出来ました。ヘッダは省略しても問題ないようです。

hello.c
#include <stdio.h>

void hello() {
	printf("hello ffi\n");
}

Haskellソースの最初の行はC言語の#pragmaディレクティブに相当するようです。foreign importの定義はP/Invokeに似ている印象です。

test2.hs
{-# LANGUAGE ForeignFunctionInterface #-}

import Foreign

foreign import ccall "hello" hello :: IO ()

main = do hello

ghcはC言語も受け付けるので、コンパイルはとても簡単です。WindowsC言語コンパイルは、ghcにバンドルされたgccに振られます。

% ghc hello.c test2.hs
[1 of 1] Compiling Main             ( test2.hs, test2.o )
Linking test2.exe ...
% ./test2.exe
hello ffi

こんなに簡単にC言語と組み合わせられるとは思っていなかったので驚きました。

FFI(ダイナミックリンク)

C言語DLLに分離してリンクするのも簡単でした。ソースは同じです。

% i686-mingw32-gcc -shared -o hello.dll hello.c
% ghc test2.hs hello.dll
Linking test2.exe ...
% ./test2.exe
hello ffi

DLLを消すと起動できなくなるため、リンクされていることが確認できます。

% rm hello.dll
% ./test2.exe
エラー

2009-05-25

[][]二重起動禁止と通知

昨日C#で作ったものと同じようなものをC言語で実装してみました。二重起動判定はMutexではなくFindWindow()で行っています。なるべく簡単に実装するため、.NETリモーティングの代わりにWM_SETTEXTを投げています。

#include <windows.h>
#include <tchar.h>
    
WNDPROC DefWndProc;
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int APIENTRY _tWinMain(
    HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
    int ret = 0;
    HWND hWnd = FindWindow(NULL, TEXT("SingleApp Test"));
    if (hWnd == NULL)
    {
        hWnd = CreateWindow(TEXT("LISTBOX"), TEXT("SingleApp Test"),
            WS_OVERLAPPEDWINDOW | LBS_STANDARD,
            CW_USEDEFAULT, 0, 200, 200, NULL, NULL, hInstance, NULL);
        if (hWnd)
        {
            MSG msg;
            DefWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LPARAM)WndProc);
            ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd);
            while (GetMessage(&msg, NULL, 0, 0))
            {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
            ret = msg.wParam;
        }
    }
    else
    {
        TCHAR buf[64];
        wsprintf(buf, TEXT("call: %d"), GetTickCount());
        SendMessage(hWnd, WM_SETTEXT, 0, (LPARAM)buf);
    }
    return ret;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_SETTEXT:
        SendMessage(hWnd, LB_ADDSTRING, 0, lParam);
        SendMessage(hWnd, LB_SETCURSEL, SendMessage(hWnd, LB_GETCOUNT, 0, 0) - 1, 0);
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    }
    return CallWindowProc(DefWndProc, hWnd, message, wParam, lParam);
}

2009-05-19

[][]画像サイズを取得

【追記】C#移植版 ⇒ id:n7shi:20110204

C言語で外部ライブラリ(libjpeg等)を使わずにJPEGの画像サイズを取得するプログラムを書いてみました。パブリックドメインに置いておきます。

#include <stdio.h>
typedef struct { int w, h; } Size;

Size GetJpegSize(const char *jpg)
{
	Size ret = { 0, 0 };
	unsigned char buf[8];
	FILE *f = fopen(jpg, "rb");
	while (f && fread(buf, 1, 2, f) == 2 && buf[0] == 0xff)
	{
		if (buf[1] == 0xc0 && fread(buf, 1, 7, f) == 7)
		{
			ret.h = buf[3] * 256 + buf[4];
			ret.w = buf[5] * 256 + buf[6];
		}
		else if (buf[1] == 0xd8 || (fread(buf, 1, 2, f) == 2 &&
			!fseek(f, buf[0] * 256 + buf[1] - 2, SEEK_CUR))) continue;
		break;
	}
	if (f) fclose(f);
	return ret;
}

int main(int argc, char *argv[])
{
	int i;
	for (i = 1; i < argc; i++)
	{
		Size sz = GetJpegSize(argv[i]);
		printf("%s: %d, %d\n", argv[i], sz.w, sz.h);
	}
	return 0;
}

2007-09-25

[]IEEE 754 単精度浮動小数点数

C言語で浮動小数点数を算出するプログラムを書いてみました。

/* This program is in the public domain. */

#include <stdio.h>
#include <limits.h>

#define EXP_DIGIT 8
#define EXP_MAX ((1 << EXP_DIGIT) - 1)
#define SGNF_DIGIT 23
#define SGNF_MAX ((1 << SGNF_DIGIT) - 1)

void printfloat(float f) {
    unsigned int v = *(int *)&f;
    printf("%f => %d-%x-%x\n", f, v >> 31, (v >> 23) & 0xff, v & 0x7fffff);
}

void Float(int s, int e10) {
    int sign = 0, e2 = 0, ss;
    if (s == 0) e10 = 0;
    if (s < 0) sign = 1, s = -s;
    while (s % 10 == 0) e10++, s /= 10;
    if (e10 < 0) {
        while (e10 != 0) {
            e2--, s <<= 1;
            ss = s % 10;
            if (ss == 0 || s > INT_MAX / 2)
                e10++, s = (s / 10) + (ss >= 5 ? 1 : 0);
        }
    } else if (e10 > 0) {
        while (e10 != 0) {
            if (s & 1 == 1 && s <= INT_MAX / 10)
                e10--, s *= 10;
            e2++, s >>= 1;
        }
    }
    while (s > 0 && s <= SGNF_MAX / 2) e2--, s <<= 1;
    while (s > SGNF_MAX * 4 + 3) e2++, s >>= 1;
    while (s > SGNF_MAX * 2 + 1) e2++, s = (s >> 1) | (s & 1);
    int result = (sign << 31) | ((e2 + SGNF_DIGIT + EXP_MAX / 2) << 23) | (s & SGNF_MAX);
    printfloat(*(float *)&result);
}

int main() {
    printfloat(-0.1); Float(-1, -1);
    printfloat(3.14159265); Float(314159265, -8);
    printfloat(16777214); Float(16777214, 0);
    printfloat(16777215); Float(16777215, 0);
    printfloat(16777216); Float(16777216, 0);
    printfloat(16777217); Float(16777217, 0);
    printfloat(16777218); Float(16777218, 0);
    return 0;
} 

二進数は自然数でしか扱ったことがなかったため、小数点表示をどう処理するか悩みました。二進数の0.1が10進数の0.5だとか、考えるだけで混乱します。そのためすべて整数だけで処理してみました。

プログラムの処理内容は以下の通りです。以下では説明の便宜上変数を a, b, c としていますが、プログラム中では s, e10, e2 に該当します。

  1. 浮動小数点数を整数 a, b により a × 10b と表す。
    例: 0.5 → 5 × 10-1 (a = 5, b = -1)
  2. c = 0 として a × 10b × 2c の形に変形する。
    例: 5 × 10-1 × 20
  3. b = 0 になるように a と c の値を調整する。
    例: 5 × 10-1 × 20 → 10 × 10-1 × 2-1 → 1 × 100 × 2-1
  4. a × 2c が得られる。
    例: 1 × 2-1
  5. 最上位ビットの位置を合わせて浮動小数点表示にする。

実際にやってみて0.1が循環小数になることが実感できました。10進数ではきれいに処理できる1/10の計算で循環が出てきたのが興味深いです。

2007-02-26

[]パイプ進捗表示ツール

ファイルを標準出力に転送しながら進捗を表示する小さなツールを作成しました。

使用例
% sizecat src.tar.bz2.1 src.tar.bz2.2 | bzcat | tar xf -
 49% [#########################-------------------------] 92/185 KB

上例のようにアーカイブを展開する時、ファイルが巨大で長時間待たされると進捗が気になります。今まではtarのvオプションで処理中のファイルを表示させていましたが、あまりに多いと参考になりません。そのためシンプルな進捗表示がしたくなり作成しました。

ソース

POSIX汎用でパブリックドメインです。

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>

#define BUFMAX 65536
static char buf[BUFMAX];

void logout(FILE *f, int pos, int size, int nosize)
{
    char bar[51];
    int i, percent, size2 = size / 100;
    
    if (size2 < 10000)
        percent = pos * 100 / size;
    else
        percent = pos / size2;
    if (percent > 100) percent = 100;
    
    for (i = 0; i < 50; i++)
        bar[i] = i * 2 < percent ? '#' : '-';
    bar[50] = '\0';
    if (nosize == 0)
        fprintf(f, "\r%3d%% [%s] %d/%d KB", percent, bar, pos / 1024, size / 1024);
    else
        fprintf(f, "\r%3d%% [%s]", percent, bar);
}

int usage(const char *argv0)
{
    fprintf(stderr, "usage: %s [-nosize] file [...]\n", argv0);
    return 1;
}

int getbufsize(int size)
{
    int sz = size / 50, a = 16, ret = 16;
    if (sz <= 16) return 16;
    if (sz >= BUFMAX) return BUFMAX;
    for (; a < sz; a <<= 1) ret = a;
    return ret;
}

int main(int argc, char *argv[])
{
    int i, size = 0, sizekb, nosize = 0, pos = 0, bufsize;

    if (argc < 2) return usage(argv[0]);

    if (!strcmp(argv[1], "-nosize"))
    {
        if (argc < 3) return usage(argv[0]);
        nosize = 1;
    }
    
    for (i = nosize + 1; i < argc; i++)
    {
        struct stat st;
        if (stat(argv[i], &st) != 0)
        {
            fprintf(stderr, "ERROR: can not get state: %s\n", argv[i]);
            return errno;
        }
        size += st.st_size;
    }
    sizekb = size / 1024;
    bufsize = getbufsize(size);

    for (i = nosize + 1; i < argc; i++)
    {
        logout(stderr, pos, size, nosize);
        FILE *f = fopen(argv[i], "rb");
        if (f == NULL)
        {
            fprintf(stderr, "\nERROR: can not open: %s\n", argv[i]);
            return errno;
        }
        while (!feof(f))
        {
            int len = fread(buf, 1, bufsize, f);
            fwrite(buf, 1, len, stdout);
            pos += len;
            logout(stderr, pos, size, nosize);
        }
        fclose(f);
    }

    fprintf(stderr, "\n");
    return 0;
}

仕組み

  • データ ⇒ 標準出力
  • 進捗情報 ⇒ 標準エラー出力

進捗情報は"\r"で行頭に戻って上書きしています。イメージを示します。

fprintf(stderr, "\r0%");
// 重い処理
fprintf(stderr, "\r50%");
// 重い処理
fprintf(stderr, "\r100%\n");