Начало программирования в Виндовс 3
Раздевая Си
И что же за всем этим стоит? На одном полюсе ассемблер, как выражение абсолютной свободы и власти. На другом, язык высокого уровня, как олицетворение невыносимой лёгкости бытия. И тот и другой имеют ряд существенных ограничений. Которые способны испортить жизнь каждого новичка.
Абсолютная власть и свобода требуют абсолютных же знаний и умений. Лёгкость бытия, в свою очередь, легка, только пока находится в пределах высокоуровневых лингвистических конструкций. Как водится, истина где-то по середине. Уже давно, со времён Turbo C, предпринимались, не ставшие очень популярными, попытки скрестить первое со вторым. Вот наиболее завершённые. HLA - хай лэвл ассемблер. C--, ассемблер, обличённый в синтаксис языка Си.
А зачем, собственно? Затем, что верхи до сих пор не могут, а низы уже давно не хотят. Я так точно не хочу. Простенький тест на вшивость, который должен пройти каждый компилятор, прежде, чем быть причисленным к лику святых.
//(c) Tereshkov_OE
extern void printf(const char *, ...);
int main(void){
unsigned int a = 0;
_asm{ jmp c}
b:
_asm{
cpuid
rdtsc
mov a,eax}
printf(" %u\n", a);
c:
_asm{ jmp b}
}
В мире Windows, из доступных, (мне известны) всего два достойных. MSVC и Pelles C. MSVC не плох, но весьма специфичен. В нём много тайн и скрытых от глаз подстав и заморочек. В том числе и в виде дыр из int 3 между функциями, например:
imul ECX,ECX,02710h
add ECX,_c
mov _c,ECX
L22D: mov ESP,EBP
pop EBP
ret
int 3
int 3
int 3
int 3
int 3
int 3
int 3
int 3
int 3
int 3
int 3
int 3
int 3
int 3
int 3
_GetResults:
push EBP
mov EBP,ESP
L243: cmp dword ptr _life,0
А вот Pelles C, хоть и не имеет ассемблерный output, хороший повод выучить язык Си до конца. До уровня *.obj, Pelles C безусловно лучший. Все последующие примеры основаны именно на нём. Из сторонних утилит, понадобится только obj2asm. Ну, чтобы проверить всё самому.
Если быть точным, Pelles C имеет ассемблерный output. Надо просто поискать :). Но этот output, без допиливания, напрямую ни с чем не совместим, в том числе и с самим Pelles C. Output из obj2asm, тоже ни с чем не совместим, но выглядит на много красивее.
Кстати, в MSVC, тоже, стоит поперёк горла собственный output. Если cl.exe выдаст что-то в роде:
.686P
.XMM
include listing.inc
.model flat
INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES
PUBLIC __real@4000000000000000
PUBLIC __real@4010000000000000
PUBLIC __real@0000000000000000
CONST SEGMENT
__real@4000000000000000 DQ 04000000000000000r ; 2
CONST ENDS
CONST SEGMENT
__real@4010000000000000 DQ 04010000000000000r ; 4
CONST ENDS
CONST SEGMENT
__real@0000000000000000 DQ 00000000000000000r ; 0
CONST ENDS
получить эксэшку из такого асма не удастся. Ибо, в LIBCMT все эти __real@ уже есть. Это не надуманный пример. В пятой статье цикла приведена программка нахождения корней квадратного уравнения sqrt.c. Проведите её по схеме cl.exe > ml.exe > link.exe и убедитесь, что исполняемый файл вам не получить. Это к вопросу о венгерской нотации и тому подобной фигне. Все остальные компиляторы такие же. Рано или поздно, но обязательно упрёшься во что-нибудь. Чаще, буквально сразу.
Здесь и далее я называю Pelles C лучшим. По типу выбора, что лучше - шило или топор? Воздушный шар или презерватив? Смотря для чего. Для ассемблерных вставок и понимания того, как выглядит Си-прога изнутри, Pelles C идеален.
Все слышали о высоком пороге вхождения в Си. Происходит это по причине не очевидных языковых конструкций, разработанных Денисом Ритчи. Зачем и кому понадобилась эта неочевидность, другой вопрос. А вот избавиться от непоняток поможет дисассемблирование. obj2asm, (хоть и не без ошибок в данных, требующих ручной правки), выдаёт вполне красивый, ни с чем не совместимый :) и компактный листинг. В большинстве случаев, идеальный для учебных целей.
Начнём с отличий в переменных. Локальные и глобальные. Инициализированные и не инициализированные. При конструировании программы, следует учитывать, что конвертеры объектных файлов и библиотек зачастую не поддерживают не инициализированные глобальные переменные, вообще.
Глобальные инициализированные
//1.c (c) Tereshkov_OE
int LookAtMe=67;int AtMeToo=45; int AtMeTwo=29;
int num[]={ 112, 12, 1222, 12333, 11, 123, 1 };
void main(void)
{
LookAtMe = LookAtMe+AtMeToo+AtMeTwo;
}
------------------------------------
public _LookAtMe
public _AtMeToo
public _AtMeTwo
public _num
public _main
_DATA segment
_LookAtMe:
db 043h,000h,000h,000h ;
_AtMeToo:
db 02dh,000h,000h,000h ;
_AtMeTwo:
db 01dh,000h,000h,000h ;
_num:
db 070h,000h,000h,000h ;
db 00ch,000h,000h,000h ;
db 0c6h,004h,000h,000h ;
db 02dh,030h,000h,000h ;
db 00bh,000h,000h,000h ;
db 07bh,000h,000h,000h ;
db 001h,000h,000h,000h ;
_DATA ends
_TEXT segment
_main:
mov EAX,_LookAtMe
add EAX,_AtMeToo
add EAX,_AtMeTwo
mov _LookAtMe,EAX
ret
_TEXT ends
end
-------------------------------------
Глобальные не инициализированные
//2.c (c) Tereshkov_OE
int LookAtMe, AtMeToo, AtMeTwo;
int num[7];
void main(void)
{
LookAtMe=67; AtMeToo=45; AtMeTwo=29;
num[1]=112; num[2]=12; num[3]=1222;
num[4]=12333; num[5]=11; num[6]=123;
num[7]=1;
LookAtMe = LookAtMe+AtMeToo+AtMeTwo;
}
---------------------------------------
comm _num:byte:01ch
comm _AtMeTwo:byte:04h
comm _AtMeToo:byte:04h
comm _LookAtMe:byte:04h
public _main
_TEXT segment
_main:
mov dword ptr _LookAtMe,043h
mov dword ptr _AtMeToo,02Dh
mov dword ptr _AtMeTwo,01Dh
mov dword ptr _num[04h],070h
mov dword ptr _num[08h],0Ch
mov dword ptr _num[0Ch],04C6h
mov dword ptr _num[010h],0302Dh
mov dword ptr _num[014h],0Bh
mov dword ptr _num[018h],07Bh
mov dword ptr _num[01Ch],1
mov EAX,_LookAtMe
add EAX,_AtMeToo
add EAX,_AtMeTwo
mov _LookAtMe,EAX
ret
_TEXT ends
_BSS segment
_BSS ends
end
----------------------------------------
Локальные
//3.c (c) Tereshkov_OE
void main(void)
{
int LookAtMe, AtMeToo, AtMeTwo;
int num[7];
LookAtMe=67; AtMeToo=45; AtMeTwo=29;
num[1]=112; num[2]=12; num[3]=1222;
num[4]=12333; num[5]=11; num[6]=123;
_asm{
mov num[7],1}
LookAtMe = LookAtMe+AtMeToo+AtMeTwo;
}
-----------------------------------------
public _main
_TEXT segment
_main:
push EBP
mov EBP,ESP
sub ESP,02Ch
mov dword ptr -4[EBP],043h
mov dword ptr -8[EBP],02Dh
mov dword ptr -0Ch[EBP],01Dh
mov dword ptr -024h[EBP],070h
mov dword ptr -020h[EBP],0Ch
mov dword ptr -01Ch[EBP],04C6h
mov dword ptr -018h[EBP],0302Dh
mov dword ptr -014h[EBP],0Bh
mov dword ptr -010h[EBP],07Bh
mov dword ptr -0Ch[EBP],1
mov EAX,-4[EBP]
add EAX,-8[EBP]
mov -02Ch[EBP],EAX
add EAX,-0Ch[EBP]
mov -4[EBP],EAX
mov ESP,EBP
pop EBP
ret
_TEXT ends
end
Как видно из листинга, имена переменных и массива полностью исчезли и превратились в смещения относительно регистра EBP. А составляющие массива num от остальных локальных переменных ничем не отличаются. Вся же компания находится в адресах больших текущей вершины стека, готовой к новым call.
Локальные в двух одинаковых функциях внутри одной программы
//4.c (c) Tereshkov_OE
void one(void)
{
int LookAtMe, AtMeToo, AtMeTwo;
int num[7];
LookAtMe=67; AtMeToo=45; AtMeTwo=29;
num[1]=112; num[2]=12; num[3]=1222;
num[4]=12333; num[5]=11; num[6]=123;
_asm{
mov num[7],1}
LookAtMe = LookAtMe+AtMeToo+AtMeTwo;
}
void main(void)
{
int LookAtMe, AtMeToo, AtMeTwo;
int num[7];
LookAtMe=67; AtMeToo=45; AtMeTwo=29;
num[1]=112; num[2]=12; num[3]=1222;
num[4]=12333; num[5]=11; num[6]=123;
_asm{
mov num[7],1}
LookAtMe = LookAtMe+AtMeToo+AtMeTwo;
one();
}
------------------------------------
public _one
public _main
_TEXT segment
_one:
push EBP
mov EBP,ESP
sub ESP,02Ch
mov dword ptr -4[EBP],043h
mov dword ptr -8[EBP],02Dh
mov dword ptr -0Ch[EBP],01Dh
mov dword ptr -024h[EBP],070h
mov dword ptr -020h[EBP],0Ch
mov dword ptr -01Ch[EBP],04C6h
mov dword ptr -018h[EBP],0302Dh
mov dword ptr -014h[EBP],0Bh
mov dword ptr -010h[EBP],07Bh
mov dword ptr -021h[EBP],1
mov EAX,-4[EBP]
add EAX,-8[EBP]
mov -02Ch[EBP],EAX
add EAX,-0Ch[EBP]
mov -4[EBP],EAX
mov ESP,EBP
pop EBP
ret
_main:
push EBP
mov EBP,ESP
sub ESP,02Ch
mov dword ptr -4[EBP],043h
mov dword ptr -8[EBP],02Dh
mov dword ptr -0Ch[EBP],01Dh
mov dword ptr -024h[EBP],070h
mov dword ptr -020h[EBP],0Ch
mov dword ptr -01Ch[EBP],04C6h
mov dword ptr -018h[EBP],0302Dh
mov dword ptr -014h[EBP],0Bh
mov dword ptr -010h[EBP],07Bh
mov dword ptr -021h[EBP],1
mov EAX,-4[EBP]
add EAX,-8[EBP]
mov -02Ch[EBP],EAX
add EAX,-0Ch[EBP]
mov -4[EBP],EAX
call _one
mov ESP,EBP
pop EBP
ret
_TEXT ends
end
Выглядит одинаково, но ESP и EBP в обоих случаях инициируются разными значениями. Это покажет любой отладчик.
Те, кто прочёл предыдущую статью цикла, уже знают, что в Си нет call и gosub. В Pelles C это легко исправить.
//5.c (c) Tereshkov_OE
extern void printf(const char *, ...);
int main(void){
unsigned int a = 0;
b:
_asm{
cpuid
rdtsc
mov a,eax
call c
jmp b}
c:
printf(" %u\n", a);
_asm{ ret}
}
--------------------------------
extrn _printf
public _main
_TEXT segment
_main:
push EBP
mov EBP,ESP
sub ESP,4
mov dword ptr -4[EBP],0
LD: cpuid
rdtsc
mov -4[EBP],EAX
call L1B
jmp LD
L1B: push dword ptr -4[EBP]
push offset FLAT:@10
call _printf
add ESP,8
ret
mov EAX,0
mov ESP,EBP
pop EBP
ret
_TEXT ends
_DATA segment
@10:
db 020h,025h,075h,00ah,000h ;
_DATA ends
end
Для закрепления, разберём ещё пару примеров из предыдущей статьи.
// ТУПО СЧЁТЧИК
extern void printf (const char *_Format,...);
int main(void)
{ for (int i = 17; i; i--)
printf("%d\n", i); }
---------------------------------------------
extrn _printf
public _main
_TEXT segment
_main:
push EBP
mov EBP,ESP
sub ESP,4
mov dword ptr -4[EBP],011h
jmp L22
LF: push dword ptr -4[EBP]
push offset FLAT:@12
call _printf
add ESP,8
dec dword ptr -4[EBP]
L22: cmp dword ptr -4[EBP],0
jne LF
mov EAX,0
mov ESP,EBP
pop EBP
ret
_TEXT ends
_DATA segment
@12:
db 025h,064h,00ah,000h ;
_DATA ends
end
--------------------------------------
// ТУПО ЕЩЁ СЧЁТЧИК
extern void printf (const char *_Format,...);
int main(void)
{ int i = 17;
for (;i;){
i--;
printf("%d\n", i);} }
-------------------------------------
extrn _printf
public _main
_TEXT segment
_main:
push EBP
mov EBP,ESP
sub ESP,4
mov dword ptr -4[EBP],011h
jmp L22
LF: dec dword ptr -4[EBP]
push dword ptr -4[EBP]
push offset FLAT:@12
call _printf
add ESP,8
L22: cmp dword ptr -4[EBP],0
jne LF
mov EAX,0
mov ESP,EBP
pop EBP
ret
_TEXT ends
_DATA segment
@12:
db 025h,064h,00ah,000h ;%d..
_DATA ends
end
------------------------------------
Оба счётчика являются аналогом цикла while i!=0. Разница в месте уменьшения аргумента, расположении команды dec dword ptr -4[EBP]. До тела основной функции или после него. do-while не случится даже, если вычеркнуть jmp L22.
Счастье будет не полным, если не увидеть, как передаются параметры функции
//6.c (c) Tereshkov_OE
int one(int c, int d)
{
return c+d;
}
int main(void)
{
int a, b;
a=3; b=4;
b=one( a, b);
}
---------------------------------------
public _one
public _main
_TEXT segment
_one:
push EBP
mov EBP,ESP
mov EAX,8[EBP]
add EAX,0Ch[EBP]
pop EBP
ret
_main:
push EBP
mov EBP,ESP
sub ESP,8
mov dword ptr -4[EBP],3
mov dword ptr -8[EBP],4
push dword ptr -8[EBP]
push dword ptr -4[EBP]
call _one
add ESP,8
mov -8[EBP],EAX
mov EAX,0
mov ESP,EBP
pop EBP
ret
_TEXT ends
end
Как видно, адресация опять ведётся относительно EBP. Параметры находятся с адреса EBP+8 и по четыре в сторону увеличения адресов. Локальные переменные с адреса EBP-4 и по четыре в сторону уменьшения адресов. Почему +8 и -4? Так оно устроено. Просто примем к сведению.
Ну вот. Методика и методология ясны. Если в программе нет явных ошибок, но она упорно демонстрирует свой несгибаемый характер, незаурядный интеллект, настаивает на собственном мнении, просто берётся функция printf, дисассемблер, отладчик или obj2asm и многое выясняется само.
На фоне визуальных сред программирования, изложенное здесь выглядит не слишком футуристично, даже приземлённо, но тем не менее. Ещё, желательно выяснить, для себя, чем отличаются cdecl от stdcall, while от do-while, case от if и т.д. Но это уже сами, :).
В нетах принято поливать фекалиями Си и растекаться мыслью по древу о преимуществах ассемблера по скорости и компактности. Вы сами всё увидели. Кто-нибудь пишет на асме лучше? Pelles C идеален. Будучи слинкованы с crtdll.dll в TCC, программки имеют размер от 1.5к. Что ещё надо?
Кстати, и обычный "Hello, World!\n" далеко не исчерпал себя. Как вам такой вариант?
//7.c (c) Tereshkov_OE
int printf(const char *, ...);
void aa (void){
char *s[] = {"Hello, World!\n"};
printf(*s);}
int main(void)
{
aa();
return 0;
}
И что он доказывает? А то, что ты хоть дерись, но символьный массив "Hello, World!\n", будь он хоть 255 раз локальным, всё равно будет лежать не в области стека, а в разделе глобальных переменных. Локальной будет только ссылка на него, что есть не одно и тоже. А в MSVC 2003, запись "Hello, World!\n" будет даже ещё и продублирована. Для надёжности. Вдруг, одна потеряется. И при этом, совершенно бесплатно :) Очень, очень надёжно. Стандарт.
Герберт Шилдт явно всего не знал. Хороший учитель должен уметь и пердеть, и сморкаться. Я так считаю. Иначе, это продажный пропагандист. Не даром, ракеты на взлёте в океан падают.
Вот, как должен выглядеть канонически верный функциональный Hello, World!, сейчас.
//8.c (c) Tereshkov_OE
int printf(const int *, ...);
int aa (void){
int s[] = {0x6c6c6548,0x57202c6f,0x646c726f,0x00000a21};
printf(s);
_asm {sub ESP,4
pop eax}}
int main(void){
int s = 0x000a6425;
printf(&s,aa());
return 0;}
Но каноникам, типа Петцольда, Шилдта и компании, нет до этого дела. Хороших учителей из них не получилось. Книжки проданы, деньги получены, тупые ритуалы расписаны, а скользкие темы лежат в стороне. Не было дела до "функциональности" и в 1973-м, нет и сегодня, не будет и завтра. Смысл и цель введения функционального программирования, отобрать у обезьян call и gosub. Всё получилось.
Предусмотрительность идиотов убивает. Мне так и не удалось объявить printf, как stdcall. Ни в Pelles C, ни в MSVC. И если первый честно об этом предупреждает, то MSVC 2003 тупо молчит и тихо делает своё чёрное дело. Зато бесплатно, от щедрот барских. Не только очень надёжный, но и скромный, даже стеснительный. Короче, альтернативы ассемблеру нет. Кстати, на этом идиотическом фоне, TCC, хоть и с недоразвитым инлайн ассемблером, тоже, очень не плох.
+++++++++++++++++++++++++++++++++++++++
Кто как хочет, так и дрочит
Кому всё это надо, если есть PureBasic? Хороший вопрос. PureBasic, это тёплый и мягкий старт, простота использования. Особенно, для новичка. Всё, что написано о нём во второй статье цикла, чистейшая правда. Я и сам на ём пишу :)
Но PureBasic закрытый продукт. Процесс компиляции полностью скрыт и имеет это своей целью. Обычный пользователь всегда будет иметь дело только с тем, что торчит наружу. А чему торчать, решает только Фред, автор и владелец PureBasic.
Главная же забота Фреда, сохранить права собственности и привязанную к себе паству. Как можно дольше. Фред обидчив, молчалив и заносчив. С ним трудно разговаривать. Таким образом, PureBasic не изменится никогда. Компилируется кое-что, ну и ладно.
Главное отличие между PureBasic и Си в том, что стандартной библиотекой Си может пользоваться каждый и всегда. В Windows это crtdll.dll. Стандартной же библиотекой PureBasic не воспользуется никто и никогда. Даже сам Фред. Её просто нет и не будет.
Приобретя начальный опыт в PureBasic и TCC, рано или поздно, кроме простоты, захочется ясности, понимания и той самой свободы и абсолютной власти. Захочется, чтобы написанные утилиты работали и в Pico_XP. Чтобы ассемблерные вставки были естественным продолжением основной программы, а не шли параллельным курсом не известно куда и никогда не пересекались. Захочется приобрести навыки, которые помогут в трудоустройстве и учёбе в ВУЗе. Написать собственную операционку, наконец.
Всё это возможно в Pelles C. Высококлассный продукт. Почти стандарт. Который, на уровне компиляции, в большинстве случаев, не станет обламывать и сдерживать инициативу. Вперёд!
Однако.
Отгремели фанфары, стихли аплодисменты. И вот, моё новое, шедевральное творение в одну секунду превращает Pelles C v8 в полуфабрикат. Равно, как и Digital Mars C++, и Orange C 386. Кстати, Digital Mars и Orange C 386, тоже, проходят предложенную выше проверку на вшивость.
//^Z F6.c (c) Tereshkov_OE
int LookAtMe=0;
void main(void)
{
a:
LookAtMe = getchar();
putchar(LookAtMe);
printf (" %d\n",LookAtMe) ;
goto a;
}
---------------------------------------
extrn _printf
extrn _putchar
extrn _getchar
public _LookAtMe
public _main
_DATA segment
_LookAtMe:
db 000h,000h,000h,000h ;....
_DATA ends
_TEXT segment
_main:
push EBP
mov EBP,ESP
L3: call _getchar
mov _LookAtMe,EAX
push dword ptr _LookAtMe
call _putchar
add ESP,4
push dword ptr _LookAtMe
push offset FLAT:@10
call _printf
add ESP,8
jmp short L3
pop EBP
ret
_TEXT ends
_DATA segment
@10:
db 020h,025h,064h,00ah,000h ; %d..
_DATA ends
end
Гениально! Не правда ли? Отбилдите это в Pelles C, Orange C или Digital Mars C++. Запустите. Введите "0123456789", энтер, чтобы убедиться, что всё работает. А теперь, нажмите кнопочку F6. Должен появиться ^Z. Теперь, энтер. Сюрприз-сюрприз. Рерайтеры.
Умоляет ли этот дефект достоинства Pelles C, Orange C и Digital Mars C++? Нисколько! И то не очень серьёзно и это. Просто, теперь их легкомыслие доказано. Как компиляторы, они всё равно хороши, наверное. Но только до уровня *.obj.
А далее, кому как повезёт. Начинается уровень функций и библиотек, которых вы не писали, не знаете, как они устроены, как работают, не понимаете их природу, а следовательно, ничего не гарантируете. Ну прямо мечта Герберта Шилдта.
И если *.obj от Pelles C и TCC, на примитивном уровне, можно использовать где угодно, то чудак из Digital Mars зачем-то тыкает везде __acrtused, __acrtused_winc, __acrtused_dll, __acrtused_con, __wacrtused, __wacrtused_con. Превращая *.obj от Digital Mars в никому не нужное, унылое г?вно. В добавок, в Digital Mars ещё и линкер не отвязно висит на snn.lib, как ведьма под перекладиной. А Digital Mars, это бывший Symantec C++. Макинтошцы 90-х, принимают поздравления. Компилятор языка D, видимо, такой же жлобский. Orange C 386 version 6.0.40, на уровне объектных файлов, вообще ни с чем не совместим. Возможно, что и с самим собой. Отдельная планета.
Мораль той басни такова. MSDN: «If a thread created using CreateThread calls the CRT, the CRT may terminate the process in low-memory conditions.» Если Microsoft, во всей её красе, великолепии и могуществе, при наличии всех исходников и всей полноты информации, расписываясь в собственной не состоятельности, не может написать адекватные CRT и CreateThread и поэтому, may=позволяет, даёт разрешение CRT самой делать то, что той вздумается, то куда уже нашим доморощенным писакам, изобретающим намеренно ни с чем не совместимые велосипеды. И первый ответ которых на любой вопрос - А зачема тибе? Отъ?бись!
Действительно, а зачема? Pelles C не может открыть файл с именем «^ZF6.c». Notepad может, а Pelles C и MSVC - нет. И я, даже, не буду спрашивать у авторов, почему. И так знаю. По причине большого ума господ аффторов. Отвяжусь сразу.
Лучше рассмотрим простую программку за авторством Дениса Ритчи. Чувствуете неизъяснимый трепет? Рука мастера! Я её немного подправил, вы уж извините и вот почему. Ни для кого не секрет, кто такие копирасты. И если сказать, что все программисты на Си и вообще все программисты копирасты, многие обидятся. Но, когда человек бездумно помещает в программу строку #include <stdio.h> или использует чужую библиотеку, что он, по вашему, на самом деле делает?
#define EOF (-1)
main()
{
int c;
c = getchar();
while (c != EOF) {
putchar(c);
c = getchar();
}
}
На этот монументальный шедевр натыкаешься сразу, как только начинается обучение. Как утверждает Ритчи, «при наличии функций getchar и putchar, ничего больше не зная о вводе-выводе, можно писать удивительно много полезных программ.» А через 20 лет ему вторит Герберт Шилдт, «нужно лишь знать, что делает та или иная функция, не интересуясь,как именно она выполняет свою задачу.» Какая правильная и удивительно верная мысль у обоих, «ничего не знать»! Файлы типа stdio.h, как раз для этого и предназначены. Особенно, на начальном этапе обучения. Меньше знаешь, крепче спишь.
Но вот программка запущена. В удивительном тыкании клавиш проходит час, месяц, год. И вот, даже самых преданных и стойких начинают одолевать сомнения. Бл?дь! А какого х?я эта ё?аная программа не завершается?! В мучительных раздумьях проходят недели, месяцы. И вот, наконец, начинает вырисовываться смутное предположение. Потому, что файл типа бесконечен. EOF не наступает. Но это же очевидная чушь. Так что же?
Может быть, ввод с клавиатуры не является файлом? Или у такого файла не бывает конца? Самое время вспомнить о найденной мной волшебной кнопочке F6, которая как раз этот самый конец фала EOF и формирует. Запускаем, тыкаем F6, ентер. Программка благополучно завершается. Выход найден? - нет! Набираем 777, F6, ентер. Программка не завершается, строка не переводится. Набираем 777, F6, F6, F6, ентер. Выглядит, как «777^Z^Z^Z». Программка не завершается, строка не переводится, на выходе«777_», как и в прошлый раз.
Поздравим себя. Только что выявлен очевидный баг getchar() из msvcrt.dll. Если в буфере более одного символа и символ EOF не первый на выход, EOF и всё, что после него, включая ентер, становится не доступным и на выходе не появляется. Чтение из буфера прекращается и getchar() переходит в режим ожидания нового ввода с клавиатуры. Этот дефект не возможно обойти и программки, типа написанной Денисом Ритчи, становятся совершенно бесполезными. Обнуляет ли при этом свой буфер getchar() не известно. Возможно, что нет.
У вежливых людей, дефекты принято называть особенностями и сопровождать их радостными воплями - «надо же, как интересно получилось!», наполнять бокалы и праздновать, праздновать! А как известно из второй статьи цикла, весь официальный фарш в мире Си пишется под одну копирку, присланную ЦРУ и Пентагон т.е. централизованно. Стало быть, остальные библиотеки CRT такие же. Все до одной. Без обид, но копирасты.
Если предположить, что getchar() из msvcrt.dll работает правильно, в соответствии со спецификациями, то значит Денис Ритчи был плохим программистом, не тестировал своих программ, ничего не знал и знать не хотел, и не скрывал этого, на пару с Гербертом Шилдтом. И вас тому же и учил.
Но если и Ритчи молодец. И getchar() из msvcrt.dll работает верно, в соответствии со спецификациями, то значит в ANSI Си комитете заседают одни дураки, которые эти спецификации пишут. И гнать их оттуда давно пора взашей и грязной метлой, вместе с их спецификациями. Что верно, решать вам. Тем более, что внутренних протестов, ни по одному из трёх вариантов, как-то не возникает.
Уже давно, проели мозги необходимостью спецификаций, комментариев и хорошего стиля. Так почему же Ритчи сам не описал правила использования его чудесной программки. Мол тыкаешь, тыкаешь, тыкаешь, а в конце, значит, ентер-F6-ентер и good buy. Ну, видимо, потому, что Великий Ритчи никогда свою программку сам и не запускал. Учить, это одно. А вот делать всё самому, это совсем другое.
И это простейшая getchar(), которой уже почти 50 лет. В Виндовс и Линух таких функций миллионы. И все они тщательно протестированы. Очень тщательно. Все до одной. Так же, как и getchar(). Вот и ракеты падают, в доказательство.
Ну всё есть. Вот только пользоваться нечем. В смысле, не возможно. И всё вместе, это называется высоким порогом вхождения. А не пошли бы вы?! Вместе с вашими порогами. Ну какие Spectre, Meltdown, Prime95 или PortSmash CVE-2018-5407, если даже простейшая getchar() толком не работает. Придуркам, работающим за колбасу, хакеры не страшны. Такие придурки сами, хуже хакеров.
Можно работать через любой прокси, но Google Chrome всегда заботливо отошлёт в Google.com ваш реальный IP. И если провайдер подвесил весь микрорайон на один адрес, развлечение вида
гарантировано через каждые 10 минут.
У меня два варианта. Либо провайдер тупой, либо Google. Но не стоит заморачиваться. Оба варианта подходят. Google повзрослел. Или захерел. Пора менять смартфон обратно на велосипед и удочки :)
GCC, пройдёт лишь кающийся:
.comm _LookAtMe, 4, 2
.comm _AtMeToo, 4, 2
.comm _AtMeTwo, 4, 2
.comm _num, 200, 5 <<<<<<<<<<<<
.text
.globl _main
.def _main;.scl 2;.type 32; .endef
_main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
call ___main <<<<<<<<<<<<<<<<<
L39:
movl $0, _you
movl $0, me
subl $12, %esp<<<<<<<<<<<<<<<<<<
pushl $LC1
call printf
addl $16, %esp
subl $12, %esp<<<<<<<<<<<<<<<<<<
pushl $LC10
call printf
addl $16, %esp
subl $12, %esp<<<<<<<<<<<<<<<<<<
pushl $LC11
call printf
addl $16, %esp
Безупречно хороших и идеально сговорчивых компиляторов нет и не существует. Не предвидится даже для обучающего применения. Ну, может быть, HLA или Fasm. Но HLA, имея широкие возможности для собственной настройки и изучения вами английского:), труден для освоения и нигде не используется. А Fasm, будучи в том числе и одной из утилит HLA, не шьёт объектные файлы, хоть и знает формат elf.obj.
За 20 лет существования, под Fasm не написано ни одного совместимого дисассемблера или парсера с других дисассемблеров и не будет. Видимо, чтобы красноглазики развивали свой кругозор и не зацикливались на одном единственном. Good reason.
Главный признак хорошего компилятора наличие трёх главных опций. Первая -do_what_i_told. Вторая -exactly_what_i_told. И третья -close_your_mouth_right_now. Хотя, опция -w, как правило, везде уже есть.
Не нравится? - не пользуйся! Yes? А я и не пользуюсь. Точнее, пользуюсь. Но только тем, что воспринимаю. Но странно, что другие пишут их компиляторы и операционки только за тем, чтобы я ими не пользовался. Не понимал, не покупал и никому не советовал.
Ну ладно бы я, а то ведь и все не пользуются. Побродив по программистским форумам, можно очень быстро составить список из функций и библиотек, которыми лучше никогда не пользоваться. Хотя бы той же CreateThread.
Сама собой возникает мысль об искусстве. Пора дать определение. «Искусство программирования»,- неизъяснимая способность писать правильные программы на основе неправильных компиляторов, кривого железа, глючных операционок, дефектных библиотек и ущербной, а часто вовсе отсутствующей документации. Ничего не забыл? :)
Линкуйте свои *.obj только с проверенными *.dll и библиотеками. Работайте только в проверенных вами компиляторах. Дисассемблируйте. Скомпануйте свой компилятор сами. И будете работать спокойно, сбережёте силы, нервы и здоровье. Будущее начального обучения, всё равно, за PureBasic и TCC >:). Жаль только потраченное впустую время. По совокупности признаков, размер, возможности и удобство, лично я, двумя руками голосую за Pelles C в формате pocc.exe. Продолжение следует.
(с) Терешков Олег Евгеньевич. Март 2019.