2011年9月15日木曜日

64bit環境で動かない.NETアプリ

一部の特殊なアプリを除けば、64bit環境でも従来のアプリはWow64上で普通に動作するものが多いが、たまに.NETアプリは動かないものがある。フックやシェル拡張など他のプロセスに寄生するアプリやドライバならともかく.NETでそんなの書かないのに何故? と不思議に思うだろう。

どういう訳かというと.NETアプリの実行ファイルはコンパイルした段階では純粋なマシン語ではない中間言語で、実行時にCPUのビット数に合ったマシン語に変換される仕組みになっており、同じ実行ファイルでも64bit OS上では64bit, 32bit OS上では32bitで動作する。

ここに一つの罠があって、.NETアプリにはたまにネイティブのDLLを利用しているものがあり、このDLLは始めからマシン語でありコンパイル時に指定したビット数のCPUでしか動作しない。そのため、64bit環境で動作させると.NETアプリのプロセスは64bitで起動して、32bitのDLLを読み込もうとしてエラーとなってしまう。

32bitのネイティブアプリのように始めからWow64上で32bitアプリとして起動させることができれば32bitのDLLでも読み込める。実は.NETアプリのコンパイル時にTarget CPUにx86を選んでいれば、64bit環境でも32bit CPUのマシン語に変換されるため、Wow64が発動してちゃんと動いてくれる。上に書いた罠が発動するのはコンパイル時にTarget CPUにAny CPUを選んでいる場合なのだ。

最近はこのノウハウも広まってきたので、アプリ作者の人もちゃんとコンパイルオプションを選んでくれるし、今動かなくても将来のバージョンアップで動くようになるだろうけど、もう作者の人が更新を止めちゃっている場合はどうしたら良いのか。

実はコンパイルオプションを変えても実行ファイルの大部分である中間言語の部分は全く同じで、ヘッダにあるフラグがちょっと違うだけなので、バイナリエディタで書き換えてやれば良い、というのは大変なのでちゃんとツールがある。

CorFlags.exe "対象の.exe" /32BIT+

問題はこのコマンドは単体では配っていなくて、.NET FrameworkのSDKを入手してインストールしないと使えるようにならないこと。自分はVisual Studio C#のExpress Editionを使っているので始めから入っていたけど、そうじゃない人はちょっと面倒かもしれない。

2011年9月10日土曜日

64bitプロセスから32bitプロセスにDLL Injection (C言語)

ググったら32bit→64bitはNGだけど、64bit→32bitは行けるという情報があったので試したら行けた。DLL Injectionって何?という人は「別のプロセスにコードを割り込ませる3つの方法」にとても詳しく書かれているのでそちらを参考に。(こういうマニアックな記事は好きだ)

面倒なので自作アプリのソースの一部をそのまま貼る。本筋じゃない処理も混ざってるけど自力で読み飛ばし推奨。GOTO_Eは最後のラベルにgotoするマクロ。
bool pAttachRemoteThread(DWORD pid, const char* dllName, bool autoFree, bool waitRemoteSemaphoreRelease){

bool result = false;
HANDLE hThread = NULL;
PWSTR vDllPath = NULL;
HANDLE hProcess = NULL;
HANDLE hSemaphore = NULL;
HANDLE hMutex = NULL;

hProcess = OpenProcess(
PROCESS_QUERY_INFORMATION |
PROCESS_CREATE_THREAD |
PROCESS_VM_OPERATION |
PROCESS_VM_WRITE,
FALSE, pid);

if(hProcess == NULL) GOTO_E;

BOOL isWow64;
IsWow64Process(hProcess, &isWow64);

// DLLのフルパスの編集
char dllPath[MAX_PATH+1];

GetModuleFileName(g_hInst, dllPath, sizeof(dllPath));
PathRemoveFileSpec(dllPath);
PathAppend(dllPath, dllName); //このDLLと同じディレクトリ

if(_stricmp(PathFindExtension(dllPath), ".dll") == 0){ // 末尾の.dllは除去(大小文字無視)
PathRemoveExtension(dllPath);
}

#ifdef _WIN64
if(!isWow64){ // 64bitのDLL名には64を追加
strcat(dllPath, "64");
}
#else
// 32bit版ビルドで64bitのプロセスにアタッチしようとしたらエラー
BOOL iamWow64;
IsWow64Process(GetCurrentProcess(), &iamWow64);
if(iamWow64 && !isWow64) GOTO_E;
#endif

strcat(dllPath, ".dll");

// ロードするDLLのパスをリモートプロセスのメモリに書きこむ。
size_t dllPathLen = strlen(dllPath) + 1;
vDllPath = (PWSTR)VirtualAllocEx(hProcess, NULL, dllPathLen, MEM_COMMIT, PAGE_READWRITE);
if(vDllPath == NULL) GOTO_E;
if(WriteProcessMemory(hProcess, vDllPath, (PVOID)dllPath, dllPathLen, NULL) == 0) GOTO_E;

// Loadlibraryのリモートプロセスでのアドレスを取得
PTHREAD_START_ROUTINE addr = pGetProcAddress(isWow64, "LoadLibraryA");
if (addr == NULL) GOTO_E;

// DLL名 + pidが同じ処理は同時実行を抑止。
char mutexName[FILENAME_MAX + 12 + 1];
sprintf(mutexName, ":%s%d", dllName, pid);
hMutex = CreateMutex(NULL, FALSE, mutexName);
if(WaitForSingleObject(hMutex, TIMEOUT) == WAIT_TIMEOUT) GOTO_E;

// リモートの処理終了を検知するセマフォ(名前:DLL名 + pid)を用意
if(waitRemoteSemaphoreRelease){
char* semaphoreName = pGetSemaphoreName(pid, dllName);
hSemaphore = CreateSemaphore(NULL, 0, 1, semaphoreName);
free(semaphoreName);
if(hSemaphore == NULL) GOTO_E;
}

// LoadLibraryを実行して終了コード判定
hThread= CreateRemoteThread(hProcess, NULL, 0, addr, vDllPath, 0, NULL);
if (hThread == NULL) GOTO_E;

// スレッドの終了待ち
if(WaitForSingleObject(hThread, TIMEOUT) == WAIT_TIMEOUT) GOTO_E;
DWORD exitCode;
GetExitCodeThread(hThread, &exitCode);
if(exitCode == NULL) GOTO_E;

// インジェクションしたDLLがセマフォをリリースするのを待つ。
if(waitRemoteSemaphoreRelease)
if(WaitForSingleObject(hSemaphore, TIMEOUT) == WAIT_TIMEOUT) GOTO_E;

// FreeLibraryを実行
if(autoFree){
addr = pGetProcAddress(isWow64, "FreeLibrary");
if (addr == NULL) GOTO_E;

CloseHandle(hThread);
hThread= CreateRemoteThread(hProcess, NULL, 0, addr, NULL, 0, NULL);
if (hThread == NULL) GOTO_E;

if(WaitForSingleObject(hThread, TIMEOUT) == WAIT_TIMEOUT) GOTO_E;
GetExitCodeThread(hThread, &exitCode);
if(exitCode == NULL) GOTO_E;
}

result = true;

ERROR_HANDLER:
if(vDllPath != NULL) VirtualFreeEx(hProcess, vDllPath, sizeof(dllPath), MEM_RELEASE);
if(hProcess != NULL) CloseHandle(hProcess);
if(hSemaphore != NULL) {
ReleaseSemaphore(hSemaphore, 1, NULL);
CloseHandle(hSemaphore);
}
if(hMutex != NULL) {
ReleaseMutex(hMutex);
CloseHandle(hMutex);
}
if(hThread != NULL) CloseHandle(hThread);

return result;
}

ここまでは最初に紹介したリンクに書いてあることそのままなので、これで終わるならあちらを読んだ方が面白い。

ここからが問題の64bitプロセスから32bitプロセスのLoadLibraryのアドレスをどうやって知るかなのだが…
はじめはCreateToolhelp32Snapshotでkernel32のベースアドレス取ってきて、PEファイルの構造を読んで関数のRVAを足せば…と試みたのだけれど、ベースアドレスの取得が上手く行かなかった。64bitプロセスから見るとkernel32.dllはロードしておらず、WOW64関連のDLLだけをロードしているように見える。

結局、32bitプロセスを起動して取得するというしょぼい力技で対応することに。もっとスマートな方法あったら教えて欲しい。
PTHREAD_START_ROUTINE pGetProcAddress(BOOL isWow64, const char* funcName, const char* libName = "kernel32"){

if(_stricmp(libName, "user32") != 0 &&
_stricmp(libName, "kernel32") != 0 ) return NULL;

// kernel32, user32.dllの関数はどの全プロセスで同じアドレスにロードされるので
// 自プロセスで取得したアドレスを別プロセスでもそのまま使用できる。
// ただし、プロセスのbit数が異なる場合は別になるため、
// 64bitから32bitのプロセスにアタッチする場合は別の方法でアドレスを取得する。
if(!isWow64)
return (PTHREAD_START_ROUTINE) GetProcAddress(GetModuleHandle(libName), funcName);

// 二回目以降の呼び出しはキャッシュを返す。
static map<string, PTHREAD_START_ROUTINE> cache;
string key = string(libName);
key.append(funcName);
if(cache.count(key) ==1) return cache[key];

// 戻り値に32bitでの関数アドレスを返すコマンドを呼び出す。
PROCESS_INFORMATION pi;
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.cb=sizeof(si);

// コマンドプロンプトは表示させない。
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;

// このDLLと同じディレクトリ\GetProcAddress32.exe ライブラリ名 関数名
char cmdLine[MAX_PATH + 64 +1];
GetModuleFileName(g_hInst, cmdLine, sizeof(cmdLine));
PathRemoveFileSpec(cmdLine);
PathAppend(cmdLine, "GetProcAddress32.exe");
strcat(cmdLine, " ");
strcat(cmdLine, libName);
strcat(cmdLine, " ");
strcat(cmdLine, funcName);

if(CreateProcess(NULL, cmdLine, NULL,NULL, FALSE,NORMAL_PRIORITY_CLASS, NULL,NULL,&si,&pi) == NULL){
error("CreateProcess()", cmdLine);
return NULL;
}

CloseHandle(pi.hThread);
WaitForSingleObject(pi.hProcess,INFINITE);
DWORD exitCode;
GetExitCodeProcess(pi.hProcess, &exitCode);
CloseHandle(pi.hProcess);

// 実行結果をキャッシュ
PTHREAD_START_ROUTINE addr = (PTHREAD_START_ROUTINE) exitCode;
cache[key] = addr;
return addr;
}

上で呼び出しているコマンド。
#include <windows.h>

int main(int argc, char** argv)
{
if(argc != 3)
return NULL;
else
return (int) GetProcAddress(GetModuleHandle(argv[1]), argv[2]);
}

デバッグ用マクロ (C言語)

デバッグ時のみ有効なエラー出力用マクロ。何となくWindows以外でも動きそうな感じに書いてみたけど、実際に使っているのはWindowsだけ。他のOSは試してないので動かないかもれない。(→追記:一応FreeBSD gcc4.2でそれっぽく動いたのでとりあえず良しとする。)

使い方は共通のヘッダファイルに下を書いておいて、それをincludeして
debug("ProcessId=%d",GetCurrentProcessId());
とか書けば
[D] test.cpp[55] WinMain[0]: ProcessId=6508
みたいにメッセージと一緒にソースコードのファイル名や行数、関数名、エラーコードをコンソールに出力してくれる。
GOTO_Eと書くと、メッセージを吐いてからERROR_HANDLERという名前のラベルにgotoする。
#ifndef _LOG_H
#define _LOG_H

#include <stdio.h>

#ifdef _WIN32
#include <windows.h>
#define ERRCD (int)GetLastError()
#define PATH_DELIM '\\'
#else
#include <stdarg.h>
#include <errno.h>
#include <string.h>
#define ERRCD errno
#define PATH_DELIM '/'
#endif

#ifdef _WIN32
// Ctrl-Cとかでコンソールウインドウを閉じなくする。
inline BOOL WINAPI HandlerRoutine(DWORD type)
{
switch(type)
{
case CTRL_C_EVENT: //Ctrl+C
case CTRL_BREAK_EVENT: //Ctrl+Break
case CTRL_CLOSE_EVENT: //CLOSE
return TRUE;
case CTRL_LOGOFF_EVENT: //LOGOFF
case CTRL_SHUTDOWN_EVENT: //SHUTDOWN
default:
return FALSE;
}
}
#endif

inline void debuglog(const int level, const char *file, const char* func, const int line, const int errcd, const char *fmt, ...)
{
#ifdef _WIN32
static bool hasConsole = false;

if(!hasConsole) {
HWND hWnd = GetConsoleWindow();
if(hWnd == NULL) {
FILE *fp;
AllocConsole();
SetConsoleTitle((LPCSTR)"Debug");
SetConsoleCtrlHandler(HandlerRoutine, TRUE);
freopen_s(&fp, "CONOUT$", "w", stderr);
hasConsole = true;

// 「閉じる」をボタン、右クリックメニューから削除。
// コンソールウインドウを閉じられるとプロセスが終了されるのを防げないため。
hWnd = GetConsoleWindow();
HMENU hMenu = GetSystemMenu (hWnd, FALSE);
HINSTANCE hinstance = (HINSTANCE) GetWindowLongPtr (hWnd, GWLP_HINSTANCE);
DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);
DrawMenuBar(hWnd);
}
}
#endif
static char* table[] = {"[D]","[I]","[W]","[E]","[A]"};
#pragma warning(push)
#pragma warning(disable:4996)
static int limitlevel = getenv("loglevel")==NULL?0:atoi(getenv("loglevel"));
#pragma warning(pop)

if(level < limitlevel) return;

// フルパスだったらファイル名に
const char* fname = strrchr(file, PATH_DELIM);
if(fname == NULL)
fname = file;
else
fname++;

va_list argp;
fprintf(stderr, "%s %s[%d] %s[%d]: ", table[level], fname, line, func, errcd);
va_start(argp, fmt);
vfprintf(stderr, fmt, argp);
va_end(argp);
fprintf(stderr, "\n");
}

#ifdef _DEBUG
#define debug(...) debuglog(0, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);
#define info(...) debuglog(1, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);
#define warn(...) debuglog(2, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);
#define error(...) debuglog(3, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);
#define critical(...) debuglog(4, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);

#define GOTO_E {error("[ERROR] goto FUNC_END"); goto FUNC_END;}
#else

#define debug(...)
#define info(...)
#define warn(...)
#define error(...)
#define critical(...)
#define GOTO_E goto FUNC_END
#endif
#endif

2011年9月9日金曜日

lockステートメントは同じスレッドをロックしない (C#)

lockステートメントで少しハマったのでメモ。ハマったコードを単純化したサンプルが以下。こんなプログラム作らねーよというツッコミはなしで。

やってることは単純で起動時にファイルを作って、ウインドウがクリックされたらリネームして元に戻す。リネームして戻すところをlockステートメントで排他をかけている。もし処理が同時に走るとリネーム済みなのに再リネームを試みてFileNotFoundになる。そしてリネームした状態で処理を止められるようポップアップを表示している。

この状態でウインドウを連続クリックすると何が起こるだろうか? 私のようなヘボプログラマは排他待ちになりエラーなしで処理されると直感的に思ってしまった。 でも実はエラーになる。
using System;
using System.IO;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
[DllImport("user32.dll", EntryPoint = "MessageBox")]
extern static Int32 MessageBox(Int32 hWnd, string text, string caption, UInt32 type);

public Form1()
{
InitializeComponent();
if(!File.Exists(fromPath) File.Create(fromPath);
if(File.Exists(toPath) File.Delete(toPath);
}

    string fromPath = "aaa.txt";
    string toPath = "bbb.txt";

    void CriticalSection(string title){
lock (this)
{
File.Move(fromPath, toPath);
MessageBox(0, "処理中", title, 0);
File.Move(toPath, fromPath);
}
}

private void Form1_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
CriticalSection("aaaa");
else
CriticalSection("bbbb");
}
}
}

何故か? ヒントはこちら。

lock によって、あるスレッドがクリティカル セクションになっているときは、別のスレッドはコードのクリティカル セクションにはなりません。ロックされたコードを別のスレッドが使おうとすると、オブジェクトが解放されるまで待機 (ブロック) します。

MSDN lock ステートメント (C# リファレンス)


「別のスレッドは」という表現がこの現象を引き起こした正体。別に同じスレッドが同時には動くことはないんだから問題ないと思うのでは? 上のコードを以下のようにちょっと変えて試してみると良い。
              File.Move(fromPath, toPath);
MessageBox(0, ""+Thread.CurrentThread.ManagedThreadId, title, 0);
File.Move(toPath, fromPath);

同じ数字が表示されるでしょ? 要はクリティカルセクション中でスレッドを切り替えるなということ。スレッドなんて切り替えていないって? 何故、メッセージボックスで処理を止めてるのにメインウインドウの描画が更新されているのか考えてみよう。

アンマネージドコードのデバッグで手を抜いて一時的にメッセージボックスを使ったばかりに変なことにはまって無駄に時間をかけてしまった。

2011年9月6日火曜日

Bloggerの投稿画面は狭すぎる

HDのディスプレイで使っていると狭くてやっていられない。そんな訳で広くする方法を探したわけですが。
見つかったのが以下のユーザスクリプト。


でも、これ古い投稿エディタじゃないと効かない。新しい投稿エディタのメリットより、編集画面が広い方が嬉しいのでしょうがなく設定から古い投稿エディタを有効にして使ってる。画像をアップロードする時に特大に設定ができないのと、プレビューが不完全なのは困るけれど… 他力本願だけど誰か何とかしてくれないだろうか。



設定の保存にxmlファイルを使う(C#)

C#で設定ファイルを簡単に実装しようと思ったらXmlSerializerを利用して設定用クラスの情報をxmlファイルにダンプ、ロードする方法が楽だ。
// 設定用クラス
public Config{
// publicにする必要あり
public string config1 = "初期値";
}

// 起動後
// 設定ファイル読み込み
// Configの部分は設定クラス名で変わる
XmlSerializer serializer = new XmlSerializer(typeof(Config));
string confFile = Path.Combine(Application.StartupPath, Path.GetFileNameWithoutExtension(Application.ExecutablePath) + ".xml");
if (File.Exists(confFile))
{
using (FileStream fs = new FileStream(confFile, FileMode.Open))
{
Config = (Config)serializer.Deserialize(fs);
}
}
// ファイルがない場合は初期値をロード
else
{
Config = new Config();
}

// いろいろな処理をやる。

// 終了前
// 設定ファイルの保存
using (FileStream fs = new FileStream(confFile, FileMode.Create))
{
serializer.Serialize(fs, Config);
}
これだけで動作はするのだが、環境によっては処理がハングすることがあるらしい。標準のXmlSerializerでは実行時に動的コンパイルでシリアライズ処理用のバイナリを作るが、この処理がまずいみたいなので、事前にコンパイル済みのバイナリを用意してそれを利用するようにすれば良いらしい。手順をは以下の通り。
  1. 設定用クラスを含めてコンパイルしたアセンブリからシリアライズ用のDLLを作成する。具体的にはビルドイベントでビルド後のイベントに「"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\sgen.exe" /f "$(TargetPath)" /o:"$(ProjectDir)"」を追加する。
  2. 作成されたDLL(アセンブリ名.XmlSerializers.dll)を参照設定に追加する。
  3. DLLを利用してシリアライズするようにプログラムを修正する。
using Microsoft.Xml.Serialization.GeneratedAssembly;  //先頭に追加

// ConfigSerializerの部分は、設定用のクラス名によって変わる。
// XmlSerializer serializer = new XmlSerializer(typeof(Config));
ConfigSerializer serializer = new ConfigSerializer();
1つだけ自分がハマったことがあったので注意として付記しておく。DLLを参照に追加するとデバック時にDLLが見つからずにエラーにならないよう、ビルドのたびにアセンブリの出力ディレクトリに参照DLLをコピーしてくれるローカルコピーという機能がある。

この際、親切なことに「参照しているDLL名.xml」(&.pdb)という名前のファイルがあればそれもコピーしてくれる。それだけなら歓迎なのだが、何故か「アセンブリ名.xml」のファイルがあれば、一緒にコピーしてしまうという謎の仕様になっているらしい。

何がまずいかというと、このサンプルのように設定ファイル名を「アセンブリ名.xml」にしていて、sgenに/oオプションを付けずアセンブリと同じディレクトリにDLLを出力、それをそのまま参照に追加するという条件が重なると困った現象が起きる。

例えばReleaseディレクトリに出力したDLLを参照に追加したとする。Releaseビルドで一回でもデバッグを実行して設定ファイルを出力した状態で、Debugビルドに切り替えてデバッグを行うと、設定ファイルがReleaseディレクトリからDebugディレクトリにコピーされてしまい、Debugビルドでアプリが書き換えた設定が再ビルドのたびにリセットされてしまうという… これで何時間無駄にしたことか…

対策は以下のどれか
  • DubugビルドとReleseビルドで参照設定の指定をちゃんと切り替える。GUIからは無理なのでプロジェクトファイルをテキストエディタで直接編集する必要あり。
  • ローカルコピーを無効にする。参照設定のプロパティから切り替えられる。
  • 「アセンブリ名.xml」ファイルが出力されない場所にDLLを出力して、参照設定に追加する。
今回の例では3番目を採用した。

コマンドプロンプトで利用可能なフォント

使えるフォントに制限があるみたいなので、使えたやつをメモとして残しておく。(随時更新予定)

◎ 日本語フォント
◎ 英字フォント
  • Lucida Console
  • Courier New
  • Consolas (Vista以降)
→ と思ったけど抜粋して書いておく。

「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Console\TrueTypeFont」に 932. (フォントを2つ以上追加する場合はドットの数を増やしていく)という名前の文字列型のキーを作ってフォント名を設定するとコマンドプロンプトのフォントの設定画面から選べるようになる。