Pure C

Материал из Lurkmore

Перейти к: навигация, поиск

По техническим причинам запрос «С#» перенаправляется сюда.

Barbed wire new flip.pngHalt! Страница огорожена от Легиона.
Хочешь высказаться? Добро пожаловать в обсуждение.

Содержание

C (Си) — язык программирования, разработанный придуманный по приколу в расовой пиндосской компании Bell Labs в начале 1970-х годов Деннисом Ритчи. Является на сегодняшний день фактически самым низкоуровневым из языков высокого уровня, и, как следствие, предоставляет достаточно гибкие возможности по использованию ресурсов компьютера благодаря повсеместному использованию указателей и операторов приведения типа. Но на том всё и заканчивается: ООП, динамика, метапрограммирование — всё это реализовали в родственниках и потомках. Cреди программистов носит неофициальный титул «кроссплатформенного ассемблера». Ответственность за корректную работу программы целиком и полностью лежит на программисте, за что Си ненавидим быдлокодерами и, что важно, их начальством. Хорошо мотивированного project manager'а, писавшего когда-то в патлатой молодости на Java, легко можно ввести в ступор, предъявив часть проекта на Си.

Быдлокод на Си обычно чуть более, чем полностью состоит из переполняющихся буферов и битья памяти, а также является излюбленной мишенью для экспериментов кулхацкеров.

Мёртвый язык

Многие почему-то считают, что Cи мёртв. Когда-то на нём писался практически весь софт, и понятие «быть программистом» однозначно и безальтернативно включало в себя «знать Си». Сейчас, конечно же, это не так, и Си успешно вытеснили практически отовсюду. Но красноглазики не унывают, и до сих пор многие кошерные софтинки (зачастую с весьма навороченным пользовательским интерфейсом) пишутся на голом Си (Wireshark, например). Но из-за того, что практически единственным вменяемым для такого рода джедайства фреймворком является GTK (или Eclipse), на подвиг отправляются исключительно закоренелые гномосеки. Си никогда не умрёт, пока есть микроконтроллеры и нужно писать драйвера.

Есть, конечно, некоторые проблемы. Бородатые олдфаги, единственные, кто умеет писать ядра операционок, драйвера и системные службы, демонстративно отказываются учить что-то ещё. На самом деле, любой уважающий себя сишник знает не только С++, но и ещё с десяток других языков, главным образом для того, чтобы их обсирать. А на Си они любят писать потому, что код в таком случае получается короче и прямее. Поэтому ядра и драйвера в их пространстве пишутся только на Си, и ни одного на Perl’е и, тем более, PHP (что не удивительно: не пихать же интерпретатор прямо в ядро). Это злостный цинизм и несправедливая конкуренция. А системные службы написаны на Си чуть менее, чем все. Кое-кто ехидно предполагает, что наличие хоть какого-нибудь вменяемого exceptioning’а в Си могло бы исключить появление BSOD в 90-х. Но всем похуй.

Си и объекты

В середине 80-х некто Страус Трупп (наст. имя Bjarne Stroustrup, не путать с Леви Страуссом) решил продвинуть дальше идею кросскомпилируемого ассемблера. Бородачи, не сподвигнувшиеся выучить ООП [1] (а некоторые из них про него и не слышали, искренне считая все технологии после 1981 г. унылым говном), разжигают холивар, который, как правило, сводится к необходимости описывать каждый чих в ООП с одной стороны и необходимость изобретать уйму велосипедов для хоть какой-нибудь объектности с другой. Однако, как показывает практика, там, где задача предрасполагает к объектности сама по себе, не использовать готовый инструментарий в подавляющем большинстве случаев глупо.

Плюсики в названии С++ (Си Плюс Плюс) обозначают наличие в нём объектно-ориентированного программирования, реализованного, правда, с учётом местной специфики. Вообще-то исконные правила ООП, в том числе и алгоритмические, предполагают полную инкапсуляцию объектов. Объект должен сам решать, что делать когда его что-то попросят, а не выставлять наружу публичные методы, которые дёргает всякий, кому не лень. Настоящий инкапсулированный объект должен принимать снаружи сообщение, причём не в контексте вызывающего объекта, а в своём собственном. Потом думать, хочет ли он это сделать, делать это и в ответ посылать сигнал о результате действия. Хотя многим нравится, но это уже тема отдельного холивара.

Любопытно, кроме C++ у Си есть ещё один обратно-совместимый родственник: Objective-C, в котором попытка реализовать объектно-ориентированное программирование до конца увенчалась успехом. Яблочники даже на нём пишут свои поделия (в том числе и интерфейс к iPhone). Но почему-то отклика в массах это не нашло.

Си отличается крайней шустростью (быстрее только ассемблер и за столетия допиленный до совершенства фортран), то есть, конечно, шустростью отличаются программисты. Гений всегда готов написать на Cи или асме так, что будет тормозить на любом самом быстром кластере. И Cи предоставляет ему в этом просто невероятное море возможностей. Например, возможность невозбранно выстрелить себе в ногу. Только для этого надо указать на участок памяти, где лежит нога, по смещению наложить структуру, пройтись по её полям и прямым преобразованием типов (динамического тут нету) передать данные функции «выстрелить». Если что-то произойдёт не так, дадут циферку с номером ошибки. Или не дадут, если функция void. Да, try-catch конструкций тут тоже нет. Ну, то есть, если вы, конечно, хотите, то есть long jump… и даже вроде как есть библиотеки с готовыми реализациями исключений а-ля C++. Тысячи их, и все говно.

Лютая, бешеная ненависть

ТруЪ Си люто, бешено любим многими, но так же люто, бешено ненавидим еще более многими. Похоже, что середины здесь нет и никогда не будет. Некоторые предпологают что водораздел по этому вопросу проходит между теми кто умеет писать на Си, и теми кто хотел-бы уметь но не умеет.

Для начала можно упомянуть тот факт, что в C (и во всех производных языках) 1/3 будет равно 0. То есть выражение 1/3==0 истинно, а вот 1/3.0==0 — нет. При этом == как оператор равенства и знак равенства как оператор присваивания до сих пор взрывает неподготовленный специальным образом мозг чуть менее, чем напрочь, по ходу и в наше время являясь источником массы трудноуловимых ошибок у умников, которые не искореняют и даже не читают варнинги.

Также доставляют строки. Со строками в обычных языках ничего не имеют общего, все функции работы перекочевали прямо из ассемблера. По умолчанию C-шная строка — это указатель char* на первый символ массива символов. Когда с ней надо что-то сделать, стандартные функции просто берут все ячейки от первой, до той, где будет следующий символ \0. Это порождает ту самую ошибку переполнения буфера (хакеры мстительно передавали на вход длинные строки, оставляя конец массива без нолика, и наивный робот грузил в строку всё подряд, пока не добирался наконец до конца), а также лютую путаницу с кодировками (char очень-очень долго был однобайтным...). Кто пытался с этим что-то сделать, тот знает, почему «если в С++ в конце концов не появится стандартного класса строк, то на улицах прольется кровь».

Во вторую очередь он ненавидим професси-аналами. Они его люто, бешено ненавидят по целому ряду причин.

Во-первых, потому, что отладка в нем может быть сильнейшей еблей мозгов, особенно когда чего-то выходит из своих границ и естественно входит в границы чего-то другого. Сейчас (на самом деле давно уже) стало полегче, а в старые добрые DOS-овские времена запуск некошерного кода зачастую приводил к полному зависанию ящика с необходимостью нажимать «Any Key» и ненажимание кнопки «Save» (в общем-то, кнопки тогда были не везде и обычно надо было давить Ctrl+S, F2 или чего еще) до запуска оной некошерной программы каралось ее некошерным перенабиванием и перепрограммированием в кошерную.

Кроме выхода за границы недозволенного была еще такая штука, как утечка памяти. Это вообще не ловилось никакими отладчиками, и нужно было долго (иногда очень) и вдумчиво (иногда очень) вчитываться в текст такой гадской программы, чтобы понять, куда эта блядская память течет[2]. Опять-таки теперь сильно полегчало. И не потому, что професи-аналы стали круче, а потому, что памяти стало в 9000 раз больше и сейчас никого ниибёт, ну и до кучи такая штука как w:Sandbox появилась. А вот в старые DOS-овские времена любой профи легко мог доказать, что 640К не хватит на всех.

Следующая замечательная вещь — рекурсия. И если продвинутый погромист мог даже объяснить, как считается C(m, n) рекурсивной функцией, то отладить рекурсивную прогу из более, чем трех строчек было выше его погромистских возможностей. Хороший, годный компилятор умеет оптимизировать хвостовую рекурсию, но сам язык этого не гарантирует.

Ну и, наконец, указатели. Это полный, ну совсем полный, просто терминальный пиздец. И ежели кто сомневается в этом и считает, что рекурсия вставляет больше, так вот они примеры:

  • double (*(*f)(double(*)(double)))(double) — указатель f на функцию, принимающую указатель на функцию, принимающую и возвращающую действительное число, возвращающую указатель на функцию, принимающую и возвращающую действительное число.
  • int (**f)(char *с) — двойной указатель на функцию, принимающую строку и возвращающую целое число
  • int *(*f)(char *с) — указатель на функцию, возвращающую указатель на целое
  • библиотечная void (* signal(int __sig, void (* __func)(int))) (int) из signal.h возвращает указатель на функцию.

В стандарте предусматривается произвольная вложенность подобного матана, причем объясняется довольно просто. Наличие гибкого механизма использования указателей вообще и указателей на функции в частности, при грамотном подходе позволяет реализовывать крайне элегантные технические решения, совершенно несопровождаемые в дальнейшем.

Такая лютая, бешеная ненависть к фундаментальным понятиям языка не могла пройти незамеченной всякими Майкрософтами. Венцом мелкософтовской ненависти к труЪ Си является появление С#, который на самом деле собственная версия Жабы.

Переполнение буфера

За техническими тонкостями просим курить педивикию, здесь же напишем суть и почему эта суть является катастрофой.

Как уже было сказано, в C реализовано прямое управление памятью. Это значит, теоретически, что программа имеет полное право изменить любой участок собственного кода и данных, когда все это висит в памяти. Причем код и данные в памяти располагаются вперемешку. И вот, допустим, программа принимает от анонимуса 10 байтов распознавания капчи. А в 11-м байте ВНЕЗАПНО находится инфа для процессора, какой код выполнять дальше.

Пока анонимус вводит 10 или меньше байтов, все хорошо. Но если на месте анонимуса окажется кулхацкер, вводящий вместо капчи 10 байт своего кода, плюс 11-й байт с указанием процессору начать выполнять этот код (уже невозбранно попавший в память, предназначенную для капчи), а C-погромист прозевал проверку на длину получаемых и записанных в память данных — то всё, кулхацкер получил доступ к программе и теперь может легко заменить весь ее код своим (в том числе запросив у компьютера еще хоть гигабайт памяти на дополнительные нужды, благодаря все той же прямой работе с памятью).

Особенно кошерно получается, если дырявая программа исполняется из-под корневого пользователя и ОС позволяет ей вообще все, вплоть до форматирования самой себя, что было совершенно нормальным явлением для всех ОС от Некрософта до XP включительно. Впрочем, в Висте и Топоре работа с правами настолько уебищна для пользователя, что разграничение прав многие просто отключают, с предсказуемым результатом. А теперь вспомним, что чуть менее чем все ОС и другие популярные и общеупотребимые программы написаны на C/C++…

Короче говоря. Причиной появления 99% дыр в программах, а значит, и всех вирусов и троянов, которые их используют, является то, что эти программы написаны на языке C и прочих низкоуровневых языках с прямым доступом к памяти.

С более современными (или просто написанными без возможности управления памятью) языками, вроде Явы, Сишарпа, Перла, Питона и даже ПХП устроить подобную дыру невозможно, как правило, даже теоретически — ну разве что она окажется внутри самого компилятора/интерпретатора. Ну да за этим намного легче уследить, чем за over 9000 программ, написанных на C-подобном. С другой стороны, у подобных языков есть другие проблемы и другие виды дыр. Ну и сборщик мусора притормаживает (но для 99% программ это не критично).

Увы, но C все еще популярен, особенно для написания системного кода, поэтому нас всех ждет еще много веселых и радостным (для хэккеров) дней.

Фауна

gcc

Ярчайшим представителем является его винраршешство GCC (вернее, правда, будет «gcc»). Изначально название расшифровывалось как GNU C Compiler, однако по мере скрещивания ужа с ежом, то есть добавления фронт-ендов к другим языкам программирования, оперативно был переименован в GNU Compiler Collection, что не есть Ъ. Многие поколения красноглазых (и не очень) верующих внесли свою лепту. А первая версия была написана самим Пророком лично. Так-то.

Отличительной особенностью является то, что именно этот компилятор в 95% случаев является вообще самой первой программой, выполненной свеженаписанным ядром любой свежевысранной ОС. И только если кернел выполняет без проблем бинарники, собранные gcc, ОС вообще может в принципе считаться годной и, таким образом, переходит в стадию Self-hosting (то есть из-под неё можно хоть как-то в принципе работать, написать первый драйвер, к примеру). Точнее, в случае если gcc не идёт, допиливают именно ОС, а не компилятор, что кагбэ намекает нам на качество и надёжность кода, им производимого. Таким образом найти ОС, на которой он не пойдёт, намного сложнее, чем жизнь на этом вашем Марсе, нередки случаи, когда вся ОС вместе со всей периферией в полном составе писалось на С при помощи него родного. За это любим всеми сведущими в С.

clang

Попытка сделать современный gcc без огромного количество legacy-кода, накопившегося для поддержки забытых систем на забытых платформах. Создаётся совместно Google, Apple и много кем ещё.

Больше, чем C

Языку уже больше 40 лет, и целые блоки кода стали в нём 100% стандартны. Были вполне многообещающие попытки оформить синтаксис C в виде ISO-стандарта, так оно каноничнее выйдет. Так-же было немало попыток сделать языки, которые компилируются в C, который компилируется уже в Assembler. Наиболее эпичны и значимы:

  • С++ — самый-самый первый компилятор Cfront от Страуструпа именно так и работал: брался код на тогдашнем C++ и перегонялся в C. Первый блин, но он стал не только комом, но и эльфом, и даже экзешником. После долгих доработок, расширений стандарта и попыток сохранить совместимость, научился компилироваться прямо в Assembler и стал намного сложнее оригинала.
  • Lua — скриптовый язык с португальскими корнями. Прост и приятен, похож на Python или JavaScript. А ещё встроен как стандарт во многие MMORPG, поэтому на нём часто пишут ботов
  • vala — для тех, кто задолбался писать под GTK на чистом C.
  • Cyclone — попытка запихнуть все расхожие шаблоны в новые функции и конструкции. Получилось чисто, но использовать всё равно тяжеловато.
  • Rust — Cyclone для простых смертных. Сейчас развивается настолько бурно, что от коммита к коммиту могут пропадать ключевые слова.

Эзотерика

Вычисление i-го числа из ряда Фибоначчи с непредсказуемым поведением программы в итоге. Из серии «я знаю, что в циклах в С/С++ можно писать всякую эзотерическую поебень»:

int i=8, a1, a2;
for (a1=a2=1; i>2; a1=(a2+=a1)-a1)
  /*
    быдлокодеры думают, что это мозгоебалка для школьника,
    а на самом деле а1=а2, а небыдло не забывает что такое +=
  */
  i--;

Проверка: является ли n степенью 2:

!(n & (n-1))

Преобразование типа к "bool":

!!x

Классика: достаём нулевой символ из строки. Нумерация элементов массива начинается с нуля:

"abcd"[0]

Один из способов проверки С или С++, помимо идентификатора __cplusplus. Алсо, многие реализации С до C99 не признают однострочных комментариев // языка С++, что может быть использовано для различия двух братских языков:

printf("%s",sizeof('C')==sizeof(int)?"C":"C++");

Выбор функции:

#include <math.h>
 
result = (use_cos ? cos : sin)(M_PI);

Копирование строк:

while( *dst++ = *src++ ) ;

Тут и так все понятно:

m(f,a,s)char*s;
{char c;return f&1?a!=*s++?m(f,a,s):s[11]:f&2?a!=*s++?1+m(f,a,s):1:f&4?a--?
 putchar(*s),m(f,a,s):a:f&8?*s?m(8,32,(c=m(1,*s++,"Arjan Kenter. \no$../.\""),
 m(4,m(2,*s++,"POCnWAUvBVxRsoqatKJurgXYyDQbzhLwkNjdMTGeIScHFmpliZEf"),&c),s)):
 65:(m(8,34,"rgeQjPruaOnDaPeWrAaPnPrCnOrPaPnPjPrCaPrPnPrPaOrvaPndeOrAnOrPnOrP\
nOaPnPjPaOrPnPrPnPrPtPnPrAaPnBrnnsrnnBaPeOrCnPrOnCaPnOaPnPjPtPnAaPnPrPnPrCaPn\
BrAnxrAnVePrCnBjPrOnvrCnxrAnxrAnsrOnvjPrOnUrOnornnsrnnorOtCnCjPrCtPnCrnnirWtP\
nCjPrCaPnOtPrCnErAnOjPrOnvtPnnrCnNrnnRePjPrPtnrUnnrntPnbtPrAaPnCrnnOrPjPrRtPn\
CaPrWtCnKtPnOtPrBnCjPronCaPrVtPnOtOnAtnrxaPnCjPrqnnaPrtaOrsaPnCtPjPratPnnaPrA\
aPnAaPtPnnaPrvaPnnjPrKtPnWaOrWtOnnaPnWaPrCaPnntOjPrrtOnWanrOtPnCaPnBtCjPrYtOn\
UaOrPnVjPrwtnnxjPrMnBjPrTnUjP"),0);}
 
main(){return m(0,75,"mIWltouQJGsBniKYvTxODAfbUcFzSpMwNCHEgrdLaPkyVRjXeqZh");}

Да, и это тоже программа. А в C++ и C99 эти примеры работать не будут, ибо нет неявного int:

#include <stdio.h>
main (int t, int _, char *a)
{
return!0<t?t<3?
main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a)):1,t<_?main(t+1,_,a) 
:3,main(-94,-27+t,a)&&t==2?_<13?main(2,_+1,"%s %d %d\n"):9:16:t<0?t<-72? 
main(_,t,"@n'+,#'/*s{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n\ 
{n+,/+#n+,/# ;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l q#'+d'K#!\ 
/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# ){nl]!\ 
/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' iwk{KK{nl]!/\ 
w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c ;;{nl'-{}rw]'/+,}##'*}\ 
#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# }'+}##(!!/") 
:t<-50?_==*a?putchar(31[a]):
main(-65,_,a+1):
main((*a=='/')+t,_,a+1):
0<t?main(2,2,"%s")
:*a=='/'||main(0,main(-61,*a,
"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry" 
),a+1);
}

Компилировать gcc-3.4, полученная программа при запуске сгенерирует ядро, которое нужно запустить grub'ом. Ещё нужен fs.tar. Инструкция по сборке и запуску.

См. также

Примечания

  1. ООП закладывалось еще в 60-е, когда его толком и реализовывать-то было не на чем, поэтому его и не замечали.
  2. Справедливости ради нужно отметить, что тем же самым страдают все остальные языки общего назначения, в которых не реализован механизм автоматической сборки мусора. А особо одарённые быдлокодеры умудряются устраивать memory leaks даже в самомусоросборной Жабе путём формирования так называемых «островов» в памяти — коллекций связанных между собой объектов, которые, однако, уже не используются самой программой.


Источник — «http://lurkmore.to/Pure_C»