Финансы Сайт на котором знают все про финансы

Пишем на русском в Native Mode

Недавно у меня возникла необходимость писать на русском в нативном приложении, но, как оказалось, добиться этого не так-то просто.
Для того, чтобы получить возможность вывода русских букв, нужно разобраться, где и в каком формате хранятся глифы символов, которые отображаются на экране. Если подробнее рассмотреть функцию winx_printf (в своем проекте я использовал ZenWINX и NDK для упрощения разработки приложения), то мы увидим, что она в свою очередь вызывает winx_print, далее вызывается NtDisplayString, которая преобразует входящую строку с помощью RtlUnicodeStringToOemString, далее идет вызов функции InbvDisplayString, которая обращается к VGA Boot Driver (bootvid.dll).

Отображаемые глифы хранятся в bootvid.dll в следующем формате (на примере английской буквы A):

00000000 — 0×00
00000000 — 0×00
00011000 — 0×18
00011000 — 0×18
00100100 — 0×24
00100100 — 0×24
00100100 — 0×24
01111110 — 0×7E
01000010 — 0×42
10000001 — 0×81
00000000 — 0×00
00000000 — 0×00
00000000 — 0×00

Посмотреть остальные символы можно с помощью следующего нехитрого скрипта на Perl:

open F, '<', 'bootvid.dll' or die $!;
#0x1938 - начало таблицы с глифами в XP SP3
seek F, 0x1938, 1;
read F, my $buf, 13 * 256;
close F;

for(my $i = 0; $i < 13 * 256; $i += 13)
{
	my $symbol = substr $buf, $i, 13;
	my $hex = unpack 'H*', $symbol;
	for(my ($j, $k) = (0, length $hex); $j < $k; $j += 2)
	{
		my $line = hex substr $hex, $j, 2;
		printf "%08b - 0x%02X\n", $line, $line;
	}
	print "\n\n";
}

Таким образом, каждый символ имеет размер 8×13 пикселей и, соответственно, занимает 13 байт. Всего под символы отведено 256 * 13 = 3328 байт. То есть, чтобы добавить поддержку русского, необходимо найти начало таблицы глифов в памяти и заменить неиспользуемые символы своими глифами. Начало таблицы может меняться в зависимости от версии ОС, например, в Windows 7 смещение от начала составляет 0x2610, в Vista 0x2420, а в XP SP3 0x1938. Найти таблицу довольно просто, для этого достаточно найти в памяти первый глиф (0x00, 0x00, 0x3C, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x3C, 0x00, 0x00, 0x00).
Для начала необходимо составить свою таблицу глифов, чтобы заменить ею часть существующей таблицы. Вручную «рисовать» такое довольно муторно, поэтому я поступил следующим образом: вывел в консоли windows список необходимых символов, сделал скриншот и преобразовал его в эдакий ASCII-арт.

Делается это следующим образом:

use GD;

my $im = GD::Image->newFromPng('image.png', 1) or die;

for(my $x = 1; $x < $im->width(); $x += 8)
{
	for(my $dy = 0; $dy < 13; $dy++)
	{
		for(my $dx = 0; $dx < 8; $dx++) { my $index = $im->getPixel($x + $dx, $dy);
			my ($r, undef, undef) = $im->rgb($index);
			if($r == 192)
			{
				print "1";
			}
			else
			{
				print "0";
			}
		}
		print "\n";
	}
	print "\n\n";
}

И сразу же сворачиваем получившуюся таблицу в массив байт:

open F, '<', 'table.txt' or die $!;
chomp(my @lines = );
close F;

for(my ($i, $j) = (0, scalar @lines); $i < $j; $i += 15)
{
	print "{";
	for my $line(@lines[$i..$i + 12])
	{
		my $int = unpack 'C', pack 'B8', $line;
		printf "0x%02X,", $int;
	}
	print "},\n";
}

Конечно, последние два скрипта можно объединить в один, но так нагляднее. Также можно заметить, что у меня в консоли выведен не только русский алфавит. Это связано с тем, что по-умолчанию русские буквы не располагаются непрерывно в шрифте (0x80 — 0xAF и 0xE0 — 0xF1), поэтому проще захватить весь интервал (0x80 — 0xF1).

Теперь, когда у нас есть готовая таблица, нам необходимо написать код, который найдет и перезапишет необходимый фрагмент памяти.
Сначала мы должны найти bootvid.dll и адрес, по которому он загружен:

NTSTATUS InitRussian()
{
	PRTL_PROCESS_MODULE_INFORMATION minfo = NULL;
	NTSTATUS code;
	ULONG i, m_size, glyph_offset = 0, image_size = 0;
	PVOID image = NULL;

	/* Размер, необходимый для RTL_PROCESS_MODULE_INFORMATION */
	code = NtQuerySystemInformation(SystemModuleInformation, minfo, 0, &m_size);
	if(code != STATUS_INFO_LENGTH_MISMATCH)
	{
		return code;
	}

	/* Выделяем память */
	code = AllocMemory((PVOID *)&minfo, m_size);
	if(!NT_SUCCESS(code))
	{
		return code;
	}

	/* Заполняем структуру */
	code = NtQuerySystemInformation(SystemModuleInformation, minfo, m_size, NULL);
	if(!NT_SUCCESS(code))
	{
		FreeMemory(minfo, m_size);
		return code;
	}

	/* Количество элементов в структуре */
	m_size = *(PULONG)minfo;
	minfo = (PRTL_PROCESS_MODULE_INFORMATION)((PUCHAR)minfo + 4);
	
	/* Перечисляем модули */
	for(i = 0; i < m_size; i++)
	{
		if(strstr(minfo[i].FullPathName, "BOOTVID"))
		{
			image_size = minfo[i].ImageSize;

			/* Выделяем память под содержимое bootvid */
			code = AllocMemory(&image, image_size);
			if(!NT_SUCCESS(code))
			{
				FreeMemory(minfo, m_size);
				return code;
			}

Также нам понадобятся дополнительные функции, с помощью которых мы будем читать и писать в память:

NTSTATUS ReadVirtualMemory(PVOID VirtualAddress, PVOID Buffer, ULONG BufferSize)
{

	SYSDBG_VIRTUAL MemoryChunks;
	MemoryChunks.Address = VirtualAddress;
	MemoryChunks.Buffer = Buffer;
	MemoryChunks.Request = BufferSize;
	return NtSystemDebugControl(SysDbgReadVirtual, &MemoryChunks, sizeof(MemoryChunks), NULL, 0, NULL);
}

NTSTATUS WriteVirtualMemory(PVOID VirtualAddress, PVOID Buffer, ULONG BufferSize)
{
	SYSDBG_VIRTUAL MemoryChunks;
	MemoryChunks.Address = VirtualAddress;
	MemoryChunks.Buffer = Buffer;
	MemoryChunks.Request = BufferSize;
	return NtSystemDebugControl(SysDbgWriteVirtual, &MemoryChunks, sizeof(MemoryChunks), NULL, 0, NULL);
}

Теперь прочитаем память bootvid и найдем начало таблицы:

#define GLYPH_SIZE 13
char first_glyph[13] = {0x00, 0x00, 0x3C, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x3C, 0x00, 0x00, 0x00};
			/* Читаем содержимое памяти bootvid в буфер */
			code = ReadVirtualMemory(minfo[i].ImageBase, image, image_size);
			if(!NT_SUCCESS(code))
			{
				FreeMemory(minfo, m_size);
				FreeMemory(image, image_size);
				return code;
			}
			/* Ищем начало таблицы глифов */
			glyph_offset = search((char *)image, first_glyph, image_size, GLYPH_SIZE);
/* Функция поиска */
ULONG __stdcall search(char *x, char *y, unsigned int n, unsigned int m)
{
	unsigned int i;
	char first, second, *third;
	first = y[0]; 
	second = y[1]; 
	third = &y[2];

	for(i = 0; i < n; i++)
	{
		if(x[i] == first && x[i+1] == second)
		{
			if(RtlCompareMemory(&x[i+2], third, m - 2) == m - 2)
			{
				return i;
			}
		}
	}

	return 0;
}

И, наконец, переписываем часть памяти:

			if(glyph_offset != 0)
			{
				/* Смещаемся на 128 глифов вперед */
				glyph_offset += (128 * GLYPH_SIZE) + (ULONG)minfo[i].ImageBase;
				/* Записываем измененную таблицу в память */
				return WriteVirtualMemory((PVOID)glyph_offset, ru_glyph, sizeof(ru_glyph));
			}
		}
	}

	/* Освобождаем память */
	FreeMemory(minfo, m_size);
	FreeMemory(image, image_size);

	return STATUS_NOT_FOUND;
}

Таким образом, мы получили готовую функцию для добавления поддержки русского языка. Следующий код позволяет убедиться в том, что она отлично работает на XP SP3:

void __stdcall NtProcessStartup(PPEB Argument)
{
	NTSTATUS code;

	char str[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\nАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя";

	zenwinx_native_init();
	winx_init(Argument);

	winx_printf("%s\n\n", str);
	winx_getch();

	code = InitRussian();
	if(code != STATUS_SUCCESS)
	{
		winx_printf("Error: 0x%x - %d\n", code, RtlNtStatusToDosError(code));
	}

	winx_printf("%s\n\n", str);
	winx_getch();

	winx_exit(0);
	return;
}

А вот как выглядит результат работы:

Однако, у этого кода есть минус — он не работает под ОС выше XP SP3 и я пока что не разобрался, как адаптировать его под них.
Исходный код проекта: скачать.