Известно, что квайн (куайн) — программа, результатом работы которой является вывод текста самой программы. При этом предполагается, что обращения к файлу, содержащему программу, не происходит.
Квайны могут быть составлены практически на всех языках программирования за исключением, быть может, некоторых экзотических. Рассмотрим задачу составления квайна на ассемблере, предполагая, что программа должна быть Win32 приложением и должна выводить свой текст в окно.
Один и тот же машинный код может быть представлен на ассемблере по-разному. Допустим, что требуется затолкнуть в стек число 48. Соответствующая двухбайтовая инструкция может быть записана как мнемонически
push 48 (или же push 30h),
так и с использованием директивы определения данных db
db 06Ah,030h (или же db 106,48).
Возможен также вариант db 'j0'. Во всех перечисленных случаях при компиляции получается один и тот же машинный код. Написать ассемблерный квайн, содержащий мнемоническое представление инструкций, затруднительно — по-видимому, потребуются тысячи строк программы. Поэтому основная идея, которой будем руководствоваться — получение квайна с директивами db в виде
Исходник 1
(используется ассемблер MASM). Трудность в том, что напрямую написать такой квайн почти невозможно — это фактически программирование в машинных кодах. Однако начать можно с записи программы с помощью обычной ассемблерной мнемоники — см. приводимый ниже исходник 2. Важно при этом, что при компиляции исходников 1 и 2 получается один и тот же машинный код.
Исходник 2
Принцип работы ясен из приведенных в тексте комментариев. Отметим только, что программа состоит из трех частей. В первой части в стеке формируется текст квайна (т.е. исходник 1). Количество байтов, определяемых одной директивой db (следовательно, и ширина строк квайна), задается константой bStr — она установлена равной 25-и. Поскольку во время компиляции не импортируются какие-либо API функции, то во второй части динамически определяются адреса ряда функций. Вначале это адрес функции GetProcAddress, получаемый трассировкой таблицы экспорта модуля kernel32.dll. Затем — адреса остальных функций, получаемых с помощью GetProcAddress. Наконец, в третьей части осуществляется вывод квайна в окно MessageBox. Поскольку по умолчанию выводимый текст статический и его нельзя куда-либо скопировать, дополнительно устанавливается хук, позволяющий копировать текст из окна. Возможны другие варианты третьей части квайна — например, с помощью создания очереди сообщений и написания оконной функции.
Итак, исходник 2 (не являющийся квайном) необходимо сохранить в пакетном файле quine.cmd и запустить его — получается исполняемый файл quine.exe. Далее запускается quine.exe, и из появившегося окна следует, выделив мышью, скопировать текст в файл quine.asm. Исполняемый файл можно удалить. Таким образом, в quine.asm — полноценный квайн (см. исходник 1). Транслируется он с помощью
ml /c /coff quine.asm
а линкуется с помощью
link /subsystem:windows /section:.text,RW quine.obj
(в секцию кода должна быть разрешена запись). Размер len секции кода составил 820 байт.
Отладку программы я производил турбо дебаггером, но при этом дополнительно включал строку
invoke ExitProcess,0
перед точкой входа start (можно взять любую другую функцию), и строки
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
Иначе дебаггер зависает — по-видимому, из-за отсутствия таблицы импорта в исполняемом файле.
Квайны могут быть составлены практически на всех языках программирования за исключением, быть может, некоторых экзотических. Рассмотрим задачу составления квайна на ассемблере, предполагая, что программа должна быть Win32 приложением и должна выводить свой текст в окно.
Один и тот же машинный код может быть представлен на ассемблере по-разному. Допустим, что требуется затолкнуть в стек число 48. Соответствующая двухбайтовая инструкция может быть записана как мнемонически
push 48 (или же push 30h),
так и с использованием директивы определения данных db
db 06Ah,030h (или же db 106,48).
Возможен также вариант db 'j0'. Во всех перечисленных случаях при компиляции получается один и тот же машинный код. Написать ассемблерный квайн, содержащий мнемоническое представление инструкций, затруднительно — по-видимому, потребуются тысячи строк программы. Поэтому основная идея, которой будем руководствоваться — получение квайна с директивами db в виде
Исходник 1
.386
.model flat,stdcall
.code
q:
db 16-ричные коды инструкций и данных
....
db 16-ричные коды инструкций и данных
end q
(используется ассемблер MASM). Трудность в том, что напрямую написать такой квайн почти невозможно — это фактически программирование в машинных кодах. Однако начать можно с записи программы с помощью обычной ассемблерной мнемоники — см. приводимый ниже исходник 2. Важно при этом, что при компиляции исходников 1 и 2 получается один и тот же машинный код.
Исходник 2
; @echo off
; ml /c /coff quine.cmd
; pause
; if errorlevel 1 goto exit
; link /subsystem:windows /section:.text,RW quine.obj
; pause
; del quine.obj
; goto exit
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
bStr = 25 ; кол-во байтов, определяемых в одной строке квайна
qLen = len * 5 + (len / bStr + 1) * 4 + len1 + 10 ; размер исходника квайна
stLen = (qLen / 1000h + 1) * 1000h ; выделяемая в стеке память под исходник
.code
; *** Часть 1 - формирование в стеке текста исходника ***
start: cld
mov edx,[esp] ; ссылка вглубь kernel32.dll - для части 2
sub esp,stLen ; выделение области в стеке
mov edi,esp
mov stAddr,edi
lea esi,text
xor ecx,ecx
mov cl,len1
rep movsb ; копирование преамбулы '.386 .model flat ...'
lea esi,start
mov bl,bStr
cycl: mov eax,ecx ; генерация текста. Конец строки?
div bl
or ah,ah
jnz comma
mov ax,0A0Dh ; да - перевод строки и вставка директивы db
stosw
mov eax,' bd'
stosd
dec edi
jmp j1
comma: mov al,',' ; нет - вставка запятой, разделителя байтов
stosb
j1: mov al,'h' ; формирование 16-ричного представления байта - суффикс h
shl eax,16
lodsb
mov ah,al
and al,0Fh
cmp al,10
sbb al,69h
das ; младшая половина байта
xchg al,ah
shr al,4
cmp al,10
sbb al,69h
das ; старшая половина
shl eax,8
mov al,'0' ; префикс 0
stosd
inc ecx
cmp ecx,len
jb cycl
mov ax,0A0Dh ; концовка квайна 'end q'
stosw
mov eax,' dne'
stosd
mov eax,'q'
stosd
; *** Часть 2 - нахождение адресов API функций ***
xor dx,dx
c2: cmp word ptr [edx],'ZM' ; MZ-заголовок модуля kernel32?
je c1
c3: sub edx,10000h
jmp c2
c1: mov ecx,[edx+3Ch]
cmp dword ptr [edx+ecx],'EP' ; PE-заголовок?
jne c3
mov ecx,[edx+ecx+78h]
add ecx,edx ; адрес таблицы экспорта kernel32
push ecx
mov eax,[ecx+20h]
add eax,edx ; адрес таблицы адресов имен функций
xor ebx,ebx
xor ecx,ecx
cycl2: mov esi,[eax+4*ebx]
add esi,edx
lea edi,GPAname
mov cl,len2
repe cmpsb ; поиск строки GetProcAddress
je found
inc ebx
jmp cycl2
found: pop ecx
mov eax,[ecx+24h]
add eax,edx
mov bx,[eax+2*ebx] ; ординал функции GetProcAddress
mov eax,[ecx+1ch]
add eax,edx
mov ebx,[eax+4*ebx]
add ebx,edx
mov edi,edx ; EBX - адрес GetProcAddress
; EDI - база модуля kernel32 (а затем - user32)
xor ecx,ecx ; нахождение адресов остальных функций
mov cl,tlen
lea esi,tbl
cycl3: cmp cl,tlen-tlen2
jne j2
push ecx
push offset DLLname
call eax ; загрузка user32.dll с помощью вызова LoadLibrary
pop ecx
mov edi,eax
j2: dec cl
push ecx
push [esi+4*ecx]
push edi
call ebx ; получение адресов функций с помощью GetProcAddress
pop ecx
mov [esi+4*ecx],eax
or cl,cl
jnz cycl3
; *** Часть 3 - вывод исходника в окно MessageBox ***
push NULL
call dword ptr _GetModuleHandle
mov hInst,eax
call dword ptr _GetCurrentThreadId
push eax
push NULL
push offset hProc
push WH_CBT ; установка хука на системные окна
call dword ptr _SetWindowsHookEx
mov hook,eax
push MB_OK
push offset capt
push stAddr
push 0
call dword ptr _MessageBox ; окно с текстом квайна
push 0
call dword ptr _ExitProcess
; ***
hProc proc code:dword,wParam:dword,lParam:dword ; обработчик хука
local coord:RECT
pusha
cmp code,HCBT_ACTIVATE ; активация окна?
jne exit
push 0FFFFh
push wParam
call dword ptr _GetDlgItem
mov ebx,eax ; хэндл текстового поля
lea eax,coord
push eax
push ebx
call dword ptr _GetClientRect ; координаты текстового поля
add coord.right,20
push NULL
push NULL
push WM_GETFONT
push ebx
mov edi,_SendMessage
call edi ; текущий шрифт текста
mov esi,eax
push SW_HIDE
push ebx
call dword ptr _ShowWindow ; скрытие текста
push NULL
push hInst
push 0FFFFh
push wParam
push coord.bottom
push coord.right
push coord.top
push coord.left
push WS_CHILD+WS_VISIBLE+ES_MULTILINE+ES_READONLY
push stAddr
push offset cname
push WS_EX_WINDOWEDGE
call dword ptr _CreateWindowEx ; окно EDIT вместо STATIC
; для того, чтобы из него можно было копировать
push NULL
push esi
push WM_SETFONT
push eax
call edi ; установка шрифта
push hook
call dword ptr _UnhookWindowsHookEx ; снятие хука
popa
xor eax,eax
ret
exit: popa
push lParam
push wParam
push code
push hook
call dword ptr _CallNextHookEx ; на следующий обработчик
ret
hProc endp
; *** инициализированные данные
capt db 'Quine',0 ; заголовок окна
GPAname db 'GetProcAddress',0
len2 equ $ - GPAname
GDIname db 'GetDlgItem',0 ; имена используемых функций
GCRname db 'GetClientRect',0
SMname db 'SendMessageA',0
SWname db 'ShowWindow',0
CWEname db 'CreateWindowExA',0
UWHEname db 'UnhookWindowsHookEx',0
CNHEname db 'CallNextHookEx',0
SWHEname db 'SetWindowsHookExA',0
MBname db 'MessageBoxA',0
LLname db 'LoadLibraryA',0
GMHname db 'GetModuleHandleA',0
GCTIname db 'GetCurrentThreadId',0
EPname db 'ExitProcess',0
DLLname db 'user32.dll',0
cname db 'EDIT',0 ; класс окна
text db '.386',13,10,'.model flat,stdcall',13,10,'.code',13,10,'q:' ; преамбула квайна
len1 equ $ - text
tbl label dword ; таблица смещений имен функций, динамически заменяемых на адреса
_GetDlgItem dd offset GDIname ; функции из user32.dll
_GetClientRect dd offset GCRname
_SendMessage dd offset SMname
_ShowWindow dd offset SWname
_CreateWindowEx dd offset CWEname
_UnhookWindowsHookEx dd offset UWHEname
_CallNextHookEx dd offset CNHEname
_SetWindowsHookEx dd offset SWHEname
_MessageBox dd offset MBname
tbl2 label dword
_LoadLibrary dd offset LLname ; функции из kernel32.dll
_GetModuleHandle dd offset GMHname
_GetCurrentThreadId dd offset GCTIname
_ExitProcess dd offset EPname
tlen2 equ ($-tbl2) / 4
tlen equ ($-tbl) / 4 ; размер таблицы
len equ $ - start ; размер кода
; *** неинициализированные данные, в квайн не попадающие
hInst dd ? ; хэндл модуля
hook dd ? ; хэндл хука
stAddr dd ? ; адрес текста квайна в стеке
end start
:exit
Принцип работы ясен из приведенных в тексте комментариев. Отметим только, что программа состоит из трех частей. В первой части в стеке формируется текст квайна (т.е. исходник 1). Количество байтов, определяемых одной директивой db (следовательно, и ширина строк квайна), задается константой bStr — она установлена равной 25-и. Поскольку во время компиляции не импортируются какие-либо API функции, то во второй части динамически определяются адреса ряда функций. Вначале это адрес функции GetProcAddress, получаемый трассировкой таблицы экспорта модуля kernel32.dll. Затем — адреса остальных функций, получаемых с помощью GetProcAddress. Наконец, в третьей части осуществляется вывод квайна в окно MessageBox. Поскольку по умолчанию выводимый текст статический и его нельзя куда-либо скопировать, дополнительно устанавливается хук, позволяющий копировать текст из окна. Возможны другие варианты третьей части квайна — например, с помощью создания очереди сообщений и написания оконной функции.
Итак, исходник 2 (не являющийся квайном) необходимо сохранить в пакетном файле quine.cmd и запустить его — получается исполняемый файл quine.exe. Далее запускается quine.exe, и из появившегося окна следует, выделив мышью, скопировать текст в файл quine.asm. Исполняемый файл можно удалить. Таким образом, в quine.asm — полноценный квайн (см. исходник 1). Транслируется он с помощью
ml /c /coff quine.asm
а линкуется с помощью
link /subsystem:windows /section:.text,RW quine.obj
(в секцию кода должна быть разрешена запись). Размер len секции кода составил 820 байт.
Отладку программы я производил турбо дебаггером, но при этом дополнительно включал строку
invoke ExitProcess,0
перед точкой входа start (можно взять любую другую функцию), и строки
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
Иначе дебаггер зависает — по-видимому, из-за отсутствия таблицы импорта в исполняемом файле.