ДЕКОМПИЛЯЦИЯ ИЛИ ВОССТАНОВЛЕНИЕ ИСХОДНИКОВ
Прежде, чем приступить к декомпиляции q2, нужно ознакомиться с правилами
названия восстанавливаемых функций. Это можно сделать
здесь.
Разбор кода будет происходить последовательно-функция за функцией с
разбором вложенных вызовов. Задача ясна: понять как работает q2 и, по
возможности, получить его исходники на Си. Замечу, что ПОЛНОЕ восстановление
НЕВОЗМОЖНО, лишь частичное. За синтаксис исходников не отвечаю:) Начали...
1. Точка входа и что там за код такой непонятный...
Точка входа нам уже известна: 0043A7E0. По идее там и должна находиться
функция WinMain. На самом деле это не так... Вот что находится по этому
адресу:
:0043A7E0 55 push ebp
8BEC mov ebp,esp
6AFF push FFFFFFFF
68C89B4400 push 00449BC8
68280B4400 push 00440B28
64A100000000 mov eax,fs:[00000000]
50 push eax
64892500000000 mov fs:[00000000],esp
83C4A8 add esp,FFFFFFA8
53 push ebx
56 push esi
57 push edi
8965E8 mov [ebp-18],esp
FF153C914400 call [0044913C]
33D2 xor edx,edx
8AD4 mov dl,ah
891530C64700 mov [0047C630],edx
8BC8 mov ecx,eax
81E1FF000000 and ecx,000000FF
890D2CC64700 mov [0047C62C],ecx
C1E108 shl ecx,08
03CA add ecx,edx
890D28C64700 mov [0047C628],ecx
C1E810 shr eax,10
A324C64700 mov [0047C624],eax
E894570000 call QUAKE2.0043FFD0
85C0 test eax,eax
750A jne QUAKE2.0043A84A (0043A84A)
6A1C push 0000001C
E879010000 call QUAKE2.0043A9C0
83C404 add esp,00000004
:0043A84A E8710E0000 call QUAKE2.0043B6C0
85C0 test eax,eax
750A jne QUAKE2.0043A85D (0043A85D)
...
и это только часть всей, довольно большой функции (тут ~1/4 кусок). Сразу
говорю, людям не владеющим Си, а тем более ассемблером i386-го, тут делать
нечего. Тем кто знает асм по-минимому, кстати тоже... Для полного понимания
всего что было и будет написано, нужно знать:
-весь язык Си. Си с классами (т.е. Си++) не нужен.
-устройство CPU i386 и все его инструкции (ассемблер). Также нужно знать
разные фишки их использования, типа:
test eax, eax
je label1
-как Си компилирует if, for, goto, основные соглашения по вызовам
функций, как происходит передача параметров в этих соглашениях и как
они компилируются в коде. Чем отличаются глобальные переменные от локальных
и как хранятся в памяти те и другие. Как выглядит каркас функции в Си. В q2
в большинстве используется C/C++ Calling Convention-т.е. параметры
передаются справа-налево, их уничтожение происходит после вызова, а
возвращаемое значение помещается в eax(флаг cf не используется).
-иметь опыт в дизассемблировании программ для Win32.
Короче, кодеры и кракеры все это должны знать, как и то, что за функция
находится вначале всех программ, написанных на Си;) Сразу скажу, она
называется WinMainCRTStartup. Эта функция предназначена для
разного рода системной инициализации. Например в этой функции
создаются массивы и структуры, используемые библиотечными
функциями типа malloc. А где же тогда WinMain?!? Вызов WinMain
также входит в задачу этой функции. Ведь кто-то должен подготовить все параметры
для WinMain, например получить командную строку... Это и делает WinMainCRTStartup.
Разбирать её нет необходимости, потому что это мартышкин труд.
Нужно просто найти через эту функцию точку входа(адрес) WinMain. Делается
это следующим образом: как известно у функции WinMain четыре параметра.
Поэтому мы просто должны найти 4 push'а, а следующий за ними call будет
вызовом WinMain. Вот что получилось:
...
:0043A920 50 push eax ;nCmdShow
56 push esi ;lpCmdLine
6A00 push 00000000 ;hPrevInstance
6A00 push 00000000
FF1548914400 call [00449148] ;это вызов GetModuleHandleA
50 push eax ;hInstance
E80EB3FFFF call QUAKE2.00435C40 ;а это WinMain
...
значит адрес WinMain - 00435С40. Кстати откинув разбор функции
WinMainCRTStartup,
и всех её вложенных вызовов, мы избавились от лишних ~10% кода!!! Ещё одно замечание:
особо волноваться за то, что откинув инициализацию, мы лишились чего-то
интересного не стоит. Там такой НУДНЫЙ код, что уши вянут. Кто любит
изучать такой тип кода, пусть разбирает, весело проведет пол-года...
Единственное, что можно хорошее сказать про WinMainCRTStartup, так
это то, что в ней заключена вся мелкомягкая гениальность программистов
Microsoft: код написан классно и не глючит:)) Исходники можно найти в
Visual C++ 6.0 в папке src в файле crt0.c, или выдрать из Visual C++ 5.0
в файле libc.lib с помощь lib.exe. Там находится объектный файл.
А у Symantec C++ 6.1 есть такая замечательная утилита - obj2asm.exe.
2. WinMain...
:00435C40 8B442408 mov eax,[esp+08]
83EC1C sub esp,0000001C
85C0 test eax,eax
53 push ebx
55 push ebp
56 push esi
57 push edi
740C je QUAKE2.00435C5B (00435C5B)
5F pop edi
5E pop esi
5D pop ebp
33C0 xor eax,eax
5B pop ebx
83C41C add esp,0000001C
C21000 ret 0010
:00435C5B 8B4C2438 mov ecx,[esp+38]
8B442430 mov eax,[esp+30]
51 push ecx
A394CC4700 mov [0047CC94],eax ;HINSTANCE global_hInstance
E852FFFFFF call QUAKE2.00435BC0 ;(1)
83C404 add esp,00000004
E8FAF8FFFF call QUAKE2.00435570 ;(2)
8B3DA0CC4700 mov edi,[0047CCA0] ;! начало блока
89442434 mov [esp+34],eax
85C0 test eax,eax
747B je QUAKE2.00435CFF (00435CFF)
83FF7D cmp edi,0000007D
7D76 jnl QUAKE2.00435CFF (00435CFF)
33DB xor ebx,ebx
85FF test edi,edi
7E46 jle QUAKE2.00435CD5 (00435CD5)
BDC0CC4700 mov ebp,0047CCC0
:00435C94 8B4D00 mov ecx,[ebp]
BE74054500 mov esi,00450574
:00435C9C 8A01 mov al,[ecx]
8AD0 mov dl,al
3A06 cmp al,[esi]
751C jne QUAKE2.00435CC0 (00435CC0)
84D2 test dl,dl
7414 je QUAKE2.00435CBC (00435CBC)
8A4101 mov al,[ecx+01]
8AD0 mov dl,al
3A4601 cmp al,[esi+01]
750E jne QUAKE2.00435CC0 (00435CC0)
83C102 add ecx,00000002
83C602 add esi,00000002
84D2 test dl,dl
75E0 jne QUAKE2.00435C9C (00435C9C)
:00435CBC 33C9 xor ecx,ecx
EB05 jmp QUAKE2.00435CC5 (00435CC5)
:00435CC0 1BC9 sbb ecx,ecx
83D9FF sbb ecx,FFFFFFFF
:00435CC5 85C9 test ecx,ecx
7408 je QUAKE2.00435CD1 (00435CD1)
43 inc ebx
83C504 add ebp,00000004
3BDF cmp ebx,edi
7CC3 jl QUAKE2.00435C94 (00435C94)
:00435CD1 8B442434 mov eax,[esp+34]
:00435CD5 3BDF cmp ebx,edi
7526 jne QUAKE2.00435CFF (00435CFF)
C704BDC0CC4700+mov dword ptr [4*edi+0047CCC0],0044EA40
47 inc edi
C704BDC0CC4700+mov dword ptr [4*edi+0047CCC0],00450574
47 inc edi
8904BDC0CC4700 mov [4*edi+0047CCC0],eax
47 inc edi
893DA0CC4700 mov [0047CCA0],edi ;! конец блока
:00435CFF 68C0CC4700 push 0047CCC0
57 push edi
E8663BFEFF call QUAKE2.00419870 ;(3)
83C408 add esp,00000008
E8EE37FFFF call QUAKE2.00429500 ;(4)
8B2DD0914400 mov ebp,[004491D0]
8BD8 mov ebx,eax
:00435D1A A198CC4700 mov eax,[0047CC98]
85C0 test eax,eax
7519 jne QUAKE2.00435D3C (00435D3C)
A1B83D4F00 mov eax,[004F3DB8]
85C0 test eax,eax
7418 je QUAKE2.00435D44 (00435D44)
D94014 fld dword ptr[eax+14]
D81D449B4400 fcomp dword ptr[00449B44]
DFE0 fnstsw ax
F6C440 test ah,40
7508 jne QUAKE2.00435D44 (00435D44)
:00435D3C 6A01 push 00000001
FF15E4904400 call [004490E4] ;(5)-Sleep
:00435D44 6A00 push 00000000
6A00 push 00000000
6A00 push 00000000
8D4C241C lea ecx,[esp+1C]
:00435D57 6A00 push 00000000
51 push ecx
FFD5 call ebp ;(6)-PeekMessageA
85C0 test eax,eax
744C je QUAKE2.00435DA3 (00435DA3)
:00435D57 6A00 push 00000000
6A00 push 00000000
8D542418 lea edx,[esp+18]
6A00 push 00000000
52 push edx
FF15D8914400 call [004491D8] ;(7)-GetMessageA
85C0 test eax,eax
7505 jne QUAKE2.00435D71 (00435D71)
E88F28FEFF call QUAKE2.00418600 ;(8)
:00435D71 8B442420 mov eax,[esp+20]
8D4C2410 lea ecx,[esp+10]
51 push ecx
A390CC4700 mov [0047CC90],eax
FF15D4914400 call [004491D4] ;(9)-TranslateMessage
8D542410 lea edx,[esp+10]
52 push edx
FF15DC914400 call [004491DC] ;(10)-DispatchMessageA
6A00 push 00000000
6A00 push 00000000
6A00 push 00000000
8D44241C lea eax,[esp+1C]
6A00 push 00000000
50 push eax
FFD5 call ebp ;(6.2)-PeekMessageA
85C0 test eax,eax
75B4 jne QUAKE2.00435D57 (00435D57)
:00435DA3 E85837FFFF call QUAKE2.00429500 ;(11)
8BF8 mov edi,eax
8BF7 mov esi,edi
2BF3 sub esi,ebx
83FE01 cmp esi,00000001
7CF0 jl QUAKE2.00435DA3 (00435DA3)
6800000300 push 00030000
6800000200 push 00020000
E8CE480000 call QUAKE2.0043A690 ;(12)
83C408 add esp,00000008
56 push esi
E8153DFEFF call QUAKE2.00419AE0 ;(13)
83C404 add esp,00000004
8BDF mov ebx,edi
E945FFFFFF jmp QUAKE2.00435D1A
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
Функция WinMain занимает всего-лишь 544 байта, чего не скажешь про размер листинга.
Как видно, у WinMain 13 вложенных вызовов, из них шестой(PeekMessageA)
вызывается дважды... На данный момент мое дерево разбора выглядит так:
WinMain.3.20.2.4. Это где-то 1/6 часть от всего кода quake2.exe.
В самом начале происходит своеобразная проверка на Win32: для него
hPrevInstance всегда равен нулю(NULL). И если это не так (hPrevInstance!=NULL),
то мы выходим из программы. Потом в глобальной переменной HINSTANCE global_hInstance
сохраняется значение hInstance для общего (глобального) использования.
После этого вызываются подряд (1) и (2).
Они разбираются сразу на лету.. (1) частично есть в исходниках
QW(QuakeWorld-см. раздел
ИСХОДНИКИ
). Она входит в состав самой WinMain(см. файл
sys_win.c). Вот её листинг:
:00435BC0 8B442404 mov eax,[esp+04] ;параметр lpCmdLine
BA01000000 mov edx,00000001
8915A0CC4700 mov [0047CCA0],edx ;int argc'
C705C0CC470048+mov dword ptr [0047CCC0],00456348 ;char *argv[MAX_NUM_ARGVS]. argv[0]="exe"
8A08 mov cl,[eax]
84C9 test cl,cl
745E je QUAKE.00435C3D (00435C3D)
:00435BDF 81FA80000000 cmp edx,00000080
7D56 jnl QUAKE.00435C3D (00435C3D)
84C9 test cl,cl
7412 je QUAKE.00435BFD (00435BFD)
:00435BEB 80F920 cmp cl,20
7E05 jle QUAKE.00435BF5 (00435BF5)
80F97E cmp cl,7E
7E08 jle QUAKE.00435BFD (00435BFD)
:00435BF5 8A4801 mov cl,[eax+01]
40 inc eax
84C9 test cl,cl
75EE jne QUAKE.00435BEB (00435BEB)
:00435BFD 803800 cmp byte ptr [eax],00
7435 je QUAKE.00435C37 (00435C37)
890495C0CC4700 mov [4*edx+0047CCC0],eax
42 inc edx
8915A0CC4700 mov [0047CCA0],edx
8A08 mov cl,[eax]
84C9 test cl,cl
7412 je QUAKE.00435C28 (00435C28)
:00435C16 80F920 cmp cl,20
7E0D jle QUAKE.00435C28 (00435C28)
80F97E cmp cl,7E
7F08 jg QUAKE.00435C28 (00435C28)
8A4801 mov cl,[eax+01]
40 inc eax
84C9 test cl,cl
75EE jne QUAKE.00435C16 (00435C16)
:00435C28 803800 cmp byte ptr [eax],00
740A je QUAKE.00435C37 (00435C37)
C60000 mov byte ptr [eax],00
8B15A0CC4700 mov edx,[0047CCA0]
40 inc eax
:00435C37 8A08 mov cl,[eax]
84C9 test cl,cl
75A2 jne QUAKE.00435BDF (00435BDF)
:00435C3D C3 ret
90 nop
90 nop
Кстати, эта функция расположена прямо перед функцией WinMain. То есть: Кармак увидел,
что кусок WinMain из QW можно переделать в отдельную функцию и незамедлительно
это сделал, приписав её в исходниках над WinMain...
Эту функцию надо как-то назвать: пусть будет Create_Argv'. Эта функция
нарезает слова из командной строки (аргументы), переданной ей в качестве параметра в
массив argv. Количество аргументов заносится в argc'. Такой способ работы с командной строкой
используется у функции main в DOS-приложениях. Замечания:
количество аргументов не должно превышать MAX_NUM_ARGVS, которое
полагается равным 128(см. quakedef.h). И ещё: самым первым аргументом argv полагается строка "exe".
Это гарантирует ненулевой размер argc'.
Далее я попытаюсь написать
собранный исходник. Скорее всего эти две функции(WinMain и Create_Argv') как находились в
sys_win.c у QW, так там и остались у q2. То же относится и к MAX_NUM_ARGVS.
//файл sys_win.c
HINSTANCE global_hInstance;
char *argv[MAX_NUM_ARGVS];
int argc;
void Create_Argv_(LPSTR lpCmdLine)
{
argc=1;
argv[0]="exe";
while(*lpCmdLine && (argc < MAX_NUM_ARGVS))
{
while(*lpCmdLine && ((*lpCmdLine <= 32) || (*lpCmdLine>126)))
lpCmdLine++;
if(*lpCmdLine)
{
argv[argc]=lpCmdLine;
argc++;
while(*lpCmdLine && ((*lpCmdLine >32) && (*lpCmdLine <=126)))
lpCmdLine++;
if(*lpCmdLine)
{
*lpCmdLine=0;
lpCmdLine++;
}
}
}
}
/*
==========
WinMain
==========
*/
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
if(hPrevInstance)
return 0;
global_hInstance=hInstance;
Create_Argv_(lpCmdLine);
//...
}
//файл quakedef.h
#define MAX_NUM_ARGVS 128
Законченным исходником написанное назвать нельзя. В нем нехватает пары include'ов, и к
тому же правильность функций по нему не проверишь.
Тут
находится тестовая программка, которая проверяет правильность Create_Argv'.
Рабочая версия компилилась на Visual C++ 5.0. Попробуйте запустить её
из строки с такими параметрами: test.exe this is test
Что же касается (2), то это новинка. По крайней мере
в QW я её не нашел.У функции
нет входных параметров, а на выходе указатель типа char, который
сохраняется в локальной переменной. Листинг:
:00435570 A1BCBC4700 mov eax,[0047BCBC] ;static cd_check_
83EC44 sub esp,00000044
85C0 test eax,eax
56 push esi
740A je QUAKE.00435587 (00435587)
B8C0BC4700 mov eax,0047BCC0 ;char cddir_[MAX_QPATH]
5E pop esi
83C444 add esp,00000044
C3 ret
:00435587 6A01 push 00000001
FF15C8904400 call [004490C8] ;(1)-SetErrorMode
8B35C4904400 mov esi,[004490C4]
C64424053A mov byte ptr [esp+05],3A
C64424065C mov byte ptr [esp+06],5C
C644240700 mov byte ptr [esp+07],00
C705BCBC470001+mov dword ptr [0047BCBC],00000001
C644240463 mov byte ptr [esp+04],63
:004355B3 8D442404 lea eax,[esp+04]
50 push eax
68B0614500 push 004561B0
68C0BC4700 push 0047BCC0
E8E92D0000 call QUAKE.004383B0 ;(2)-sprintf
83C40C add esp,0000000C
8D4C2404 lea ecx,[esp+04]
8D542408 lea edx,[esp+08]
51 push ecx
6894614500 push 00456194
52 push edx
E8D22D0000 call QUAKE.004383B0 ;(2.2)-sprintf
83C40C add esp,0000000C
8D442408 lea eax,[esp+08]
6890614500 push 00456190
50 push eax
E8E02B0000 call QUAKE.004381D0 ;(3)-fopen
83C408 add esp,00000008
85C0 test eax,eax
7415 je QUAKE.0043560C (0043560C)
50 push eax
E8E3240000 call QUAKE.00437AE0 ;(4)-fclose
83C404 add esp,00000004
8D4C2404 lea ecx,[esp+04]
51 push ecx
FFD6 call esi ;(5)-GetDriveType
83F805 cmp eax,00000005
741C je QUAKE.00435628 (00435628)
:0043560C 8A442404 mov al,[esp+04]
FEC0 inc al
3C7A cmp al,7A
88442404 mov [esp+04],al
7E99 jle QUAKE.004355B3 (004355B3)
C605C0BC470000 mov byte ptr [0047BCC0],00
33C0 xor eax,eax
5E pop esi
83C444 add esp,00000044
C3 ret
:00435628 B8C0BC4700 mov eax,0047BCC0
5E pop esi
83C444 add esp,00000044
C3 ret
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
90 nop
У функции 5 вложенных вызовов: 2 системных и 3 библиотечных.
Что такое библиотечные функции???
Библиотечные функции - это функции Си, например printf, strcpy
итд.
Библиотечные
функции прилинковываются в самый конец проекта в процессе компиляции. А вообще
они хранятся в библиотеке libc.lib.
О том, что это библиотечные
функции можно узнать путем таких проверок: по входным параметрам и по
коду этих функций(во-первых он там слишком замороченный для
обычных программ, а во-вторых его можно найти, дизассемблировав
W32DASM'ом какую-нибудь (лучше все) msvcrtXXX.dll и поискав
совпадающие строчки кода). Потом, ещё можно смотреть по адресу:
допустим у какой-то функции адрес заключен в пределах от 00437AE0 до
004381D0 - значит это 100% библиотечная функция, так как она находится
между fclose и fopen. Короче поиск библиотечных функций я беру на
себя...
Что делает функция WinMain.2:
-вначале нужно сказать, что эта функция защищена от повторного вызова статической
переменной [0047BCBC]. То есть WinMain.2 вызывается в программе
только один раз-в самом начале. Я назвал эту защелку cd_check'.
-с помощью SetErrorMode
мы убираем обработку критических ошибок.
Такие ошибки могут возникнуть, например, если невозможно открыть файл
(повреждена поверхность диска).
-далее мы перебираем все диски с 'C' по 'Z' и ищем на них следующий файл:
x:\install\data\quake2.exe. Как известно, такая директория находится на
Quake2 CD, поэтому мы по существу определяем: вставлен ли диск с Quake2 в сидюк или нет.
Проверка наличия этого файла на диске проверяется путем попытки его открытия с помощью
fopen.
А для того чтобы удостовериться, что это точно CD-ROM (а не винт), мы используем
вызов GetDriveTypeA.
-результат поиска - путь к Quake2 на CD-ROM, который заносится в глобальный массив cddir',
размером MAX_QPATH(см. исходники gamex86.dll файл q_shared.h), поэтому WinMain.2 можно назвать get_cddir'.
Её назначение-проверка наличия Quake2 CD-ROM и получение расположения данных на нем.
Кстати, их расположение можно установить вручную, через командную строку:
quake2.exe +set cddir d:\dir.
Насчет исходника этой функции я не уверен, но попробовать можно:
//файл ???.c
char cddir_[MAX_QPATH];
char *get_cddir_(void)
{
static int cd_check_;
FILE *f;
char cdpath[MAX_QPATH];
if(cd_check_)
return cddir_;
SetErrorMode(SEM_FAILCRITICALERRORS);
cdpath[1]=':';
cdpath[2]='\\';
cdpath[3]='\0';
cd_check_=1;
//try to find quake2 CD
for(cdpath[0]='c';cdpath[0]<='z';cdpath[0]++)
{
sprintf(cddir_, "%sinstall\\data", cdpath);
sprintf(&cdpath[4], "%sinstall\\data\\quake2.exe", cdpath);
f=fopen(&cdpath[4], "r");
if(f)
{
fclose(f);
if(GetDriveTypeA(cdpath)==DRIVE_CDROM)
return cddir_;
}
}
//quake2 CD not founded
cddir_[0]='\0';
return NULL;
}
//файл q_shared.h
#define MAX_QPATH 64
Как обычно теперь будет,
здесь
лежит тестовая программка. Запустите её вначале со вставленным
Quake2 CD-ROM, а потом без него и посмотрите на результат. Примечания:
расположение функции get_cddir' в исходниках установить не удалось,
поэтому она временно будет находиться в sys_win.c...
Для полноты описания приведу внешние проявления get_cddir': при запуске
q2 кратковременно заводится CD-ROM. Это происходит из-за
get_cddir', когда она открывает на нем файл quake2.exe. Не знаю зачем нужна
эта информация, но я становлюсь спокойным, когда контролирую поведение
программы по полной;)
3. WinMain... Часть вторая.
Здесь будет происходить разбор части WinMain, заключенной в красные
восклицательные знаки. Этот блок представляет собой сложную циклическую
структуру, исходник которой получить очень сложно. Данный блок выполняется
только в том случае, если в сидюке находится quake2 CD, иначе
он просто пропускается.
В общих чертах тут происходит вот что: если в командной строке есть
определение пути к CD-данным(+set cddir d:\dir или просто cddir d:\dir),
то мы выходим из блока, если же в командной строке определение cddir
отсутствует, то в неё добавляются еще 3 аргумента: "+set", "cddir" и путь,
полученный с помощью get_cddir'. Напомню - все это выполняется только в
том случае, если вставлен диск с игрой. Таким образом блок обеспечивает
наличие переменной cddir, даже если она не задана в командной строке.
О том, что в исходниках присутсвует inline функция strcmp, я
понял только тогда, когда она встретилась мне дальше в таком виде раз пять.
//файл sys_win.c
/*
==========
WinMain
==========
*/
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
char *cddir;
int arg=0;
if(hPrevInstance)
return 0;
global_hInstance=hInstance;
Create_Argv_(lpCmdLine);
cddir=get_cddir_();
if(cddir && argc && (argc < MAX_NUM_ARGVS))
{
while(arg < argc)
if(!strcmp(argv[arg++], "cddir")) break;
if(arg == argc)
{
argv[argc++]="+set";
argv[argc++]="cddir";
argv[argc++]=cddir;
}
}
//...
}
Эта
программа выводит содержимое командной строки сразу после блока
(т.е. она проверяет правильность восстановленного
исходника). Чтобы протестить программу, запустите её вначале с диском,
а потом без него; попробуйте ввести в обоих случаях в командной строке
quake2.exe +set cddir d: или quake2.exe cddir e:\data или просто
quake2.exe this is test.
продолжение следует...
Organic
/2001/