Pull to refresh

Создание 1k/4k intro для Linux, часть 2

Reading time 23 min
Views 8.2K
Не прошло и полгода! Как вы можете, поднапрягшись, вспомнить, в прошлый раз мы остановились на унынии и обещании нырнуть в ассемблер.
Ну что же, пацан сказал — пацан сделал. Из этого аляповатого нагромождения букв вы узнаете, как можно инициализировать OpenGL-контекст в GNU/Linux в какие-то 450 байт, высвободив ещё больше места для разворачивания таланта.

Под катом вы узнаете, как в один килобайт нарисовать что-нибудь такое:


Заинтересованные пристёгиваются и вдавливают педаль в пол, а глаз — в экран.


Для начала, давайте поговорим об этом. Почему мы так плохи и никчёмны? Что же именно добавляет нам веса и тянет вниз, в пучины деградации?
Чтобы ответить на эти вопросы, нам потребуются инструменты для настоящих мужчин — readelf и objdump.

Расчехляем первый и натравливаем на оставшийся с прошлого раза файл intro — тот самый, который остаётся после обработки напильником-sstrip'ом:

$ readelf -a intro
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8048250
  Start of program headers:          52 (bytes into file)
  Start of section headers:          0 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         8
  Size of section headers:           40 (bytes)
  Number of section headers:         0
  Section header string table index: 0

There are no sections in this file.

There are no sections in this file.

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
  INTERP         0x000134 0x08048134 0x08048134 0x00015 0x00015 R   0x1
      [Requesting program interpreter: /lib32/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x005ac 0x005ac R E 0x1000
  LOAD           0x000f4c 0x08049f4c 0x08049f4c 0x000c4 0x00100 RW  0x1000
  DYNAMIC        0x000f4c 0x08049f4c 0x08049f4c 0x000a8 0x000a8 RW  0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
  GNU_RELRO      0x000f4c 0x08049f4c 0x08049f4c 0x000b4 0x000b4 R   0x1
  PAX_FLAGS      0x000000 0x00000000 0x00000000 0x00000 0x00000     0x4

Dynamic section at offset 0xf4c contains 16 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libdl.so.2]
 0x00000004 (HASH)                       0x804814c
 0x6ffffef5 (GNU_HASH)                   0x8048164
 0x00000005 (STRTAB)                     0x80481ac
 0x00000006 (SYMTAB)                     0x804817c
 0x0000000a (STRSZ)                      45 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000015 (DEBUG)                      0x0
 0x00000003 (PLTGOT)                     0x8049ff4
 0x00000002 (PLTRELSZ)                   16 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x8048210
 0x6ffffffe (VERNEED)                    0x80481e0
 0x6fffffff (VERNEEDNUM)                 1
 0x6ffffff0 (VERSYM)                     0x80481da
 0x00000000 (NULL)                       0x0

There are no relocations in this file.

There are no unwind sections in this file.

Histogram for bucket list length (total of 1 buckets):
 Length  Number     % of total  Coverage
      0  0          (  0.0%)
      1  0          (  0.0%)      0.0%
      2  1          (100.0%)    100.0%

No version information found in this file.

(конкретные цифры, размеры и смещения зависят от всего тулчейна и возраст ваших миль майская Варвара)
Что мы здесь видим? В самом начале, конечно же, стандартный ELF Header, из него слова не выкинешь. Далее мы видим, что sstrip почикал все section headers (упражнение: сравните вывод с readelf -a intro-orig, который до sstrip), что хорошо. Но дальше мы видим какой-то праздник program headers и dynamic section (at offset). Неужели они нам правда такие нужны в платье стоят красивые?

Спойлер: нет!*
(* — поправка: да, но не такие)

Разделываем эльфов

Давайте посмотрим, какого минимального размера можно соорудить корректный запускаемый эльф-файл. За основу возьмём то, что у нас есть сейчас, но просто выкинем все наши полезные кишочечки.

simple.c:
void _start(void)
{
	asm(
		"xor %eax,%eax\n"
		"inc %eax\n"
		"int $0x80\n"
	);
}

Соберём его (скрипт взят из Прошлого):
cc -Wall -m32 -c simple.c -Os -nostartfiles -o simple-c.o && \
ld -melf_i386 -dynamic-linker /lib32/ld-linux.so.2 simple-c.o -o simple-c-orig && \
cp simple-c-orig simple-c && \
sstrip simple-c && \
cat simple-c | 7z a dummy -tGZip -mx=9 -si -so > simple-c.gz && \
cat unpack_header simple-c.gz > simple-c.sh && \
wc -c simple-c.sh && chmod +x simple-c.sh && \
./simple-c.sh

Смотрим на размеры получившихся и промежуточных файлов и замечаем следующее:
  1. sstrip решает — размер стрипнутого simple-c (почти) в три раза меньше, чем размер simple-c-orig
  2. сжатие почти не играет роли — сжатый файл с кодом распаковки занимает почти столько же, сколько и несжатый
  3. (если дописать к ld параметр -s, то можно выиграть в размере нестрипнутого файла примерно на треть, никак не потеряв и не прибавив в размере промежуточного стрипнутого бинарника, и _проиграть_ 4 байта в размере финального сжатого)

Заглядывая в будущее можно заметить, что эти замечания будут бесполезны чуть менее, чем полностью, но мы-то ещё не в будущем!

Теперь можно натравить readelf на бинарник и посмотреть, что от него осталось, во что он превратился, кем он стал:
$ readelf -a simple-c
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8048094
  Start of program headers:          52 (bytes into file)
  Start of section headers:          0 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         3
  Size of section headers:           40 (bytes)
  Number of section headers:         0
  Section header string table index: 0

There are no sections in this file.

There are no sections in this file.

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x08048000 0x08048000 0x0009e 0x0009e R E 0x1000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
  PAX_FLAGS      0x000000 0x00000000 0x00000000 0x00000 0x00000     0x4

There is no dynamic section in this file.

There are no relocations in this file.

There are no unwind sections in this file.

No version information found in this file.

Так, sstrip вроде бы и молодчина, раздел нашу эльф-девицу, снял все эти ненужные заголовки секций. Но при этом наша дама ещё не готова — на ней ещё есть лифчик (заголовки GNU_STACK и PAX_FLAGS) и трусы, которые можно обнаружить, если обратить внимание на то, что FileSiz заголовка LOAD почему-то меньше, чем размер самого файла, что значит, что это не трусы вовсе, а просто мусор в конце файла, который необходимо постанывая сорвать.
Кроме того, если быть совсем уж придирчивым и декомпилировать (objdump -D, будет работать только на нестрипнутом файле, т.к. objdump читает section headers, а не program) то, что нам тут нагенерили, в высшей степени необходимо заметить не первой свежести носки из чуть более чем полностью вредных пролога и эпилога функции:
00 55               push ebp
01 89e5             mov ebp, esp

03 31c0             xor eax, eax
05 40               inc eax
06 cd80             int 0x80

08 5d               pop ebp
09 c3               ret

Соотношение 5 байт «полезного» кода к 5 — бесполезного так же не может греть наши юные сердца. А что будет греть наши юные сердца? Горящие спрессованные останки динозавров и болот. В эти самые спрессованные болота и предлагаю нам с вами, мой любознательный читатель, погрузиться.

Делаем эльфа пальцем

Как оказывается, собрать эльфа из ничего довольно несложно, надо всего-то-лишь выключить старкрафт, открыть файл /usr/include/elf.h, ничего не понять, загуглить файл elf.pdf, прочитать его по диагонали и сорвать с кончиков пальцев примерно следующее:
bits 32 ; работаем в 32-битном режиме
org 0x00040000 ; эта константа автоматически прибавляется к смещениям всех меток, также, именно это значение будет лежать в спец-константе $$
; Почему здесь лежит именно такое значение? А хер его знает. (см ниже)

; начало elf-заголовка
db 0x7f, 'ELF' ; magic для опознавания файла, иначе система не поймёт, что это ELF
db 1 ; EI_CLASS = ELFCLASS32
db 1 ; EI_DATA ELFDATA2LSB
db 1 ; EI_VERSION = EV_CURRENT
times 9 db 0 ; 9 неиспользуемых байт, которые можно загадить мусором и не переживать об этом

dw 2 ; e_type = ET_EXEC -- запускаемый файл
dw 3 ; e_machine = EM_386
dd 1 ; e_version = EV_CURRENT
dd _start ; e_entry -- адрес точки входа, с этого места начнётся выполнение программы
dd phdrs - $$ ; e_phoff -- смещение относительно начала файла, по которому находятся program headers
dd 0 ; e_shoff -- --//-- section headers, коих у нас, к слову, нет, поэтому 0
dd 0 ; e_flags -- не надо нам никаких флагов
dw ehsize ; e_ehsize -- размер ELF-заголовка (52 байта)
dw phsize ; e_phentsize -- размер одного program header (32 байта)
dw 1 ; e_phnum -- их количество
dw 0 ; e_shentsize -- размер section header
dw 0 ; e_shnum -- их количество (тютюшки)
dw 0 ; e_shstrndx -- что-то там со строками связано, нам не нужно

ehsize equ ($-$$) ; $ означает текущее смещение от начала файла (+значение org), поэтому в ehsize теперь лежит размер elf-заголовка

phdrs: ; начало program header
dd 1 ; p_type = PT_LOAD -- тип заголовка "загрузи кусок файла мне в память, скотина"
dd 0 ; p_offset -- смещение относительно начала файла, откуда читать
dd $$ ; p_vaddr -- адрес в памяти, куда писать
dd $$ ; p_paddr -- физический адрес, какая-то платформенно-специфичная фигота, я сам не разобрался, поэтому давайте не будем выпендриваться и просто повторим за мной
dd file_size ; p_filesz -- размер куска файла, который будем загружать в память
dd file_size ; p_memsz -- размер этого куска в памяти. Если больше, чем e_filesz, будет полиномиально интерполироваться побайтно. Если меньше -- преобразовываться ФНЧ Чебышева второго рода 6 порядка. Шутка. ЛОЛ!!11 На самом деле, если больше, то остаток будет забиваться нулями. А если меньше И ТУТ СУКА ВЗРЫЩ КИШКИ ОБ СТЕНУ МОЗГИ ГОВНО РАСЧЛЕНЁНКА.
dd 7 ; p_flags (=PF_RWX) -- загруженные данные можно будет читать, исполнять и изменять.
dd 0x1000 ; p_align -- выравнивание, применяется и к смещению, и к адресу в памяти. 0x1000 является довольно безопасным значением

phsize equ ($-phdrs) ; аналогично рассчитываем размер program header

_start: ; поехали
	xor eax, eax ; eax = 0
	inc eax ; eax = 1 (exit syscall)
	int 0x80 ; вызов syscall

file_size equ ($-$$) ; ну тут уже всё понятно должно быть

(На самом деле, вместо того, чтобы давать вот этот грубый, но слегонца откомментированный asm-файл, я хотел вам, мои лапочки ненаглядные, обнимаю, нарисовать большую схему ELF-формата, но потом понял, что это отложило бы статью еще на пару лет)

Компилировать этот файл нужно nasm'ом с директивой компиляции в «плоский» бинарник, без каких-либо лишних обвесок-заголовков:
$ nasm -f bin simple.asm -o simple-asm

Эта строчка создаст нам файл simple-asm размером 89 байт, который будет полным функциональным аналогом того 206-байтного монстра, что был создан нами ранее. Не говоря уже о том, что те 206 байт — это, на самом деле, 657 байт оригинального несжатого нестрипнутого безумия.
Можно
chmod +x simple-asm
и посмотреть, что он на самом деле валидный и даже запускается. Могу сказать, что 89 байт — далеко не предел, и можно этот файл удавить еще примерно в два раза — наложить elf header на единственный program header, и получить не то, чтобы супер-валидный эльф, но вполне себе запускающийся и имеющий размер меньше, чем сам elf header (последние 6 байт из него будут автоматически добиты нулями, как они и должны быть)[1]. Невольно на ум приходит то, что под линуксом есть замечательные файлы /dev/fb0 и /dev/dsp, а значит можно делать интры в 128-256-512 байт DOS-style. Мы, впрочем, не будем здесь таким заниматься — это тема для отдельного нагромождения знаков.
А будем мы заниматься тем, что разберёмся, как подключить сюда OpenGL.

Демоническая линковка

Как мы, наверное, смутновато помним из предыдущей части, для создания OpenGL-контекста мы использовали библиотеку SDL, подключавшуюся динамически, как libSDL.so. Кроме неё нам, разумеется, нужен ещё и сам OpenGL, который идёт в виде библиотеки libGL.so.
Нетрудно, как говорится, видеть, что нам нужно научиться заставлять уметь иметь возможность наш самопальный бинарник динамически линковаться с чем бы то ни было.
Механизм динамической линковки под линуксом на низком уровне очень смешной. Система при загрузке бинарника обнаруживает у него program header с типом PT_INTERP, который указывает на имя файла интерпретатора. «Да пошли вы в жопу со своей этой динамической линковкой, сами разбирайтесь, раз такие умные», — говорит система и вместо того, чтобы продолжить загружать наш бинарник, загружает интерпретатор и передаёт ему управление.
Дальнейшей загрузкой приложения занимается уже интерпретатор — он, как ни в чём ни бывало, самостоятельно загружает секции PT_LOAD в процесс и, что самое важное, читает PT_DYNAMIC, в котором находится ссылка на описание всего необходимого для динамической линковки — имена необходимых библиотек, функций, куда, и, что немаловажно, как их грузить. Формат данных, на которые ссылается PT_DYNAMIC, сам по себе довольно простой: таблица пар двойных слов d_tag и d_val, где d_tag — код параметра, а d_val — его значение, которое для многих параметров является адресом в уже загруженном в память процессе (или что-то около того).
Какие же параметры и значения нужно указывать? Давайте вернёмся наверх к readelf-дампу нашего первоначального файла и посмотрим на всё ЭТО ЧТО ЗА ПОКЕМОН?! после строчки «Dynamic section at offset ...».

Страшно!

Успокоим себя той мыслью, что треть из них совершенно нам не понадобится. Волнение, впрочем, не отступает — оставшиеся-то поля всё равно страшные. Однако же, я не буду объяснять подробно их смысл — отчасти потому, что сам уже не помню (расковыривал это достаточно давно), a то, что помню, предпочёл бы забыть.
Поэтому вам, дорог(ой/ие) читател(ь/и) я привожу готовый рецепт, аккуратно посыпанный тонким слоем комментариев по вкусу. Пользуйтесь на здоровье.

Этот кусок добавляется к phdrs сразу после расчёта phsize:
dd 2 ; p_type = PT_DYNAMIC
dd dynamic - $$ ; p_offset
dd dynamic ; p_vaddr
dd dynamic ; p_paddr
dd dynamic_size ; p_filesz
dd dynamic_size ; p_memsz
dd 6 ; p_flags = PF_RW
dd 4 ; p_align

dd 3 ; p_type = PT_INTERP
dd interp - $$ ; p_offset
dd interp ; p_vaddr
dd interp ; p_paddr
dd interp_size ; p_filesz
dd interp_size ; p_memsz
dd 4 ; p_flags = PF_R
dd 1 ; p_align

; данные для PT_DYNAMIC
; только самый необходимый минимум
dynamic:
dd 1, st_libdl_name ; DT_NEEDED -- необходимо прилинковать библиотеку с именем, лежащим в st_libdl_name (индекс в таблице символов)
; опциональные рюшечки, просящие интерпретатор заранее проверить, все ли необходимые библиотеки на месте
;dd 1, st_libSDL_name
;dd 1, st_libGL_name
dd 4, dt_hash ; DT_HASH -- указатель на таблицу хешей
dd 5, dt_strtab ; DT_STRTAB -- указатель на таблицу строк. все ссылки на строки имеют смысл смещения относительно начала таблицы строк
dd 6, dt_symtab ; DT_SYMTAB -- указатель на таблицу символов
dd 10, dt_strtab_size ; DT_STRSZ -- размер таблицы строк
dd 11, dt_symtab_size ; DT_SYMENT -- размер таблицы символов
dd 17, dt_rel ; DT_REL -- указатель на таблицу релокаций
dd 18, dt_rel_size; DT_RELSZ -- размер таблицы релокаций
dd 19, 8 ; DT_RELENT -- размер одной записи в таблице релокаций
dd 0, 0 ; DT_NULL -- конец данных для DT_DYNAMIC
dynamic_size equ $ - dynamic

; данные для DT_HASH
; эти данные нужны для чего-то вроде оптимизации загрузки миллионов импортируемых 
; функций из динамических библиотек, однако ж для наших двух функций это оверкилл
; поэтому здесь просто заглушка
dt_hash: dd 1, 3, 0, 0, 0, 0

; данные для DT_SYMTAB
; именно в этом месте мы говорим, какие функции нам нужно подгрузить динамически
dt_symtab:
; 0 -- первая запись пустая (зачем?!)
dd 0, 0, 0
dw 0, 0 ; SHN_UNDEF
; 1 'dlopen'
dd st_dlopen_name, 0, 0
dw 0x12 ; = ELF32_ST_INFO(STB_GLOBAL, STT_FUNC), т.е., короче говоря, тип символа -- глобальная функция
dw 0 ; SHN_UNDEF говорит, что этого символа у нас нет, и его надо искать вовне
; 2 'dlsym'
dd st_dlsym_name, 0, 0
dw 0x12, 0 ; --//--
dt_symtab_size equ $ - dt_symtab

; данные для DT_REL
; таблица релокаций. описывает, куда и как загружать адреса символов из таблицы символов
dt_rel:
dd rel_dlopen ; адрес, куда грузить
dd 0x0101 ; ELF32_R_INFO(1,R_386_32) : dt_symtab[1] ('dlopen'), тип = запиши адрес символа + r_addend(=0 у нас)
dd rel_dlsym ; --//--
dd 0x0201 ; ELF32_R_INFO(2,R_386_32) : dt_symtab[2] ('dlsym'), --//--
dt_rel_size equ $ - dt_rel

; сгруппируем строки вмете -- ожидаем, что так они будут лучше паковаться

; данные для DT_STRTAB
; таблица строк. в ней лежит всё, необходимое для PT_DYNAMIC -- названия библиотек и имена функций
dt_strtab:
st_libdl_name equ $ - dt_strtab ; адрес строки относительно начала таблицы строк
db 'libdl.so.2', 0 ; все строки -- нуль-терминированные
st_dlopen_name equ $ - dt_strtab
db 'dlopen', 0
st_dlsym_name equ $ - dt_strtab
db 'dlsym', 0 
dt_strtab_size equ $ - dt_strtab

; стандартный линуксовый интерпретатор для динамической линковки
interp: db '/lib/ld-linux.so.2', 0
interp_size equ $ - interp

Помимо этого надо поправить следующее:
  1. очевидно, изменить e_phnum на 3
  2. добавить в самый конец файла
    ; BSS-секция, в ней лежат неинициализированные данные
    absolute $
    bss:
    
    ; резервируем место под адреса функций
    rel_dlopen: resd 1
    rel_dlsym: resd 1
    
    mem_size equ ($-$$)
    
  3. в PT_LOAD-заголовке p_memsz установить в mem_size


Всё, теперь наш рукотворный эльф может линковаться динамически. Проверим это.
Раз уж это более не просто тест, а потенциальная интра, переименуем файл в intro.asm. Соберём его:
$ nasm -f bin intro.asm -o intro && chmod +x intro

И запустим через strace, чтобы проверить, что он действительно пытается читать всякие so'шечки:
$ strace ./intro
execve("./intro", ["./intro"], [/* 67 vars */]) = 0
[ Process PID=24135 runs in 32 bit mode. ]
...
open("/lib32/libdl.so.2", O_RDONLY)     = 3
...

Теперь можно посмотреть и на размер файла — 368 несжатых байт. Можете самостоятельно проверить, что обычным способом (cc+ld) аналогичный несжатый файл сразу раздуется до 4 килобайт.
Сколько же будет весить сжатый файл?
nasm -f bin intro.asm -o intro && chmod +x intro && \
cat intro | 7z a dummy -tGZip -mx=9 -si -so > intro.gz && \
cat unpack_header intro.gz > intro.sh && \
wc -c intro.sh && chmod +x intro.sh && \
./intro.sh

254 байта.
Но это он еще ничего не делает.

Пусть делает!

Теперь, как только мы заполучили в своё распоряжение dlopen и dlsym, можно наконец-то уже загрузить какие-нибудь функции из libSDL и libGL и попробовать что-нибудь эдакое накалякать.
Особо не выпендриваемся и просто портируем на ассемблер всё то, что мы делали на сях:
; имена библиотек и функций, которые нужно загрузить ручками
libs_to_dl:
st_libSDL_name equ $ - dt_strtab
db 'libSDL-1.2.so.0', 0 ; самое кросс-дистрибутивное название библиотеки, которое мне удалось выяснить эмпирически
db 'SDL_Init', 0
db 'SDL_SetVideoMode', 0
db 'SDL_PollEvent', 0
db 'SDL_GetTicks', 0
db 'SDL_ShowCursor', 0
db 'SDL_GL_SwapBuffers', 0
db 'SDL_Quit', 0
db 0 ; два нуля подряд = конец библиотеки
st_libGL_name equ $ - dt_strtab
db 'libGL.so.1', 0
db 'glViewport', 0
db 'glCreateShader', 0
db 'glShaderSource', 0
db 'glCompileShader', 0
db 'glCreateProgram', 0
db 'glAttachShader', 0
db 'glLinkProgram', 0
db 'glUseProgram', 0
db 'glRectf', 0
db 0, 0 ; три нуля подряд = конец загрузки

_start: ; поехали
 mov ebp, bss ; пускай ebp всё время указывает на bss -- будет удобно, поверьте!
; супер-удобные штуки, см далее
%define BSSADDR(a) ebp + ((a) - bss)
%define F(f) [ebp + ((f) - bss)]

; начнём загрузку функций
 mov esi, libs_to_dl+1 ; +1, т.к. ld_load ожидает, что мы уже вгрызлись на один символ в строку
 lea edi, [BSSADDR(libs_syms)] ; edi = адрес места, куда следует аккуратно сохранять адреса функций
ld_load:
 dec esi ; в этом месте мы уже вгрызлись в строку на 1, поэтому отступим назад
; подготовим параметры функции dlopen, они передаются через стек
 push 1 ; RTLD_LAZY
 push esi ; адрес имени библиотеки
 call F(rel_dlopen) ; eax = dlopen([esi], 1)
; обратите внимание, что здесь мы <s>не чистим после себя</s>засираем стек и РАДУЕМСЯ ЭТОМУ
 mov ebx, eax ; сохраним то, что dlopen нам вернул, в ebx

; скипаем все до 0
ld_skip_to_zero:
 lodsb
 test al, al
 jnz ld_skip_to_zero

; если следующий тоже \0 то конец текущей библиотеки
 lodsb
 test al, al
 jz ld_second_zero

 dec esi ; опять отматываемся на 1 назад
 push esi ; начало строки с названием функции
 push ebx ; возвращенный из dlopen указатель на загруженную библиотеку
 call F(rel_dlsym) ; eax = dlsym([ebx], [esi])
 stosd ; запишем eax (возвращенный указатель на функцию) в [edi], edi += 4
 jmp ld_skip_to_zero ; перемотаем до следуюшего нуля

ld_second_zero:
; если третий не ноль, то подгрузим что-нибудь еще!
 lodsb
 test al, al
 jnz ld_load

; здесь будет наша умопомрачительная интра!

; вон из Новосибирска!
 xor eax, eax ; eax = 0
 inc eax ; ex = 1 (exit syscall)
 int 0x80 ; вызов syscall

file_size equ ($-$$) ; ну тут уже всё понятно должно быть

; BSS-секция, в ней лежат неинициализированные данные
absolute $
bss:

; резервируем место под адреса функций
libdl_syms:
rel_dlopen: resd 1
rel_dlsym: resd 1

libs_syms:
SDL_Init: resd 1
SDL_SetVideoMode: resd 1
SDL_PollEvent: resd 1
SDL_GetTicks: resd 1
SDL_ShowCursor: resd 1
SDL_GL_SwapBuffers: resd 1
SDL_Quit: resd 1
glViewport: resd 1
glCreateShader: resd 1
glShaderSource: resd 1
glCompileShader: resd 1
glCreateProgram: resd 1
glAttachShader: resd 1
glLinkProgram: resd 1
glUseProgram: resd 1
glRectf: resd 1

mem_size equ ($-$$)

Этот кусок надо вставить вместо всего того, что у нас происходит сразу после _start, включая сам _start.
Компилируем, получаем файл размером 455 байт, запускаем, проверяем, что он не падает. Если падает — пробуем раскомментировать DT_NEEDED-строчки для libSDL и libGL, и смотрим, что происходит, страдаем и перестаём читать дальше.

Если всё хорошо, можно двигаться дальше и наконец-то уже инициализировать OpenGL с шейдерами. Здесь ничего особо хитрого (за исключением того, что написано в комментариях), мы просто повторяем на ассемблере то, что делали ранее на сях.
; nasm -f bin intro.asm -o intro && chmod +x intro && \
; cat intro | 7z a dummy -tGZip -mx=9 -si -so > intro.gz && \
; cat unpack_header intro.gz > intro.sh && \
; wc -c intro.sh && chmod +x intro.sh && \
; ./intro.sh

%define WIDTH   640
%define HEIGHT  360
%define FULLSCREEN 0
;%define FULLSCREEN 0x80000000

bits 32 ; работаем в 32-битном режиме
org 0x00040000 ; эта константа автоматически прибавляется к смещениям всех меток, также, именно это значение будет лежать в спец-константе $$
; Почему здесь лежит именно такое значение? А хер его знает. (см ниже)

; начало elf-заголовка
db 0x7f, 'ELF' ; magic для опознавания файла, иначе система не поймёт, что это ELF
db 1 ; EI_CLASS = ELFCLASS32
db 1 ; EI_DATA ELFDATA2LSB
db 1 ; EI_VERSION = EV_CURRENT
times 9 db 0 ; 9 неиспользуемых байт, которые можно загадить мусором и не переживать об этом

dw 2 ; e_type = ET_EXEC -- запускаемый файл
dw 3 ; e_machine = EM_386
dd 1 ; e_version = EV_CURRENT
dd _start ; e_entry -- адрес точки входа, с этого места начнётся выполнение программы
dd phdrs - $$ ; e_phoff -- смещение относительно начала файла, по которому находятся program headers
dd 0 ; e_shoff -- --//-- section headers, коих у нас, к слову, нет, поэтому 0
dd 0 ; e_flags -- не надо нам никаких флагов
dw ehsize ; e_ehsize -- размер ELF-заголовка (52 байта)
dw phsize ; e_phentsize -- размер одного program header (32 байта)
dw 3 ; e_phnum -- их количество
dw 0 ; e_shentsize -- размер section header
dw 0 ; e_shnum -- их количество (тютюшки)
dw 0 ; e_shstrndx -- что-то там со строками связано, нам не нужно

ehsize equ ($-$$) ; $ означает текущее смещение от начала файла (+значение org), поэтому в ehsize теперь лежит размер elf-заголовка

phdrs: ; начало program header
dd 1 ; p_type = PT_LOAD -- тип заголовка "загрузи кусок файла мне в память, скотина"
dd 0 ; p_offset -- смещение относительно начала файла, откуда читать
dd $$ ; p_vaddr -- адрес в памяти, куда писать
dd $$ ; p_paddr -- физический адрес, какая-то платформенно-специфичная фигота, я сам не разобрался, поэтому давайте не будет выпендриваться и просто повторим за мной
dd file_size ; p_filesz -- размер куска файла, который будем загружать в память
dd mem_size ; p_memsz -- размер этого куска в памяти. Если больше, чем e_filesz, будет полиномиально интерполироваться побайтно. Если больше -- преобразовываться ФНЧ Чебышева второго рода 6 порядка. Шутка. ЛОЛ!!11 На самом деле, если больше, то остаток будет забиваться нулями. А если меньше И ТУТ СУКА ВЗРЫЩ КИШКИ ОБ СТЕНУ МОЗГИ ГОВНО РАСЧЛЕНЁНКА.
dd 7 ; p_flags (=PF_RWX) -- загруженные данные можно будет читать, исполнять и изменять.
dd 0x1000 ; p_align -- выравнивание, применяется и к смещению, и к адресу в памяти. 0x1000 является довольно безопасным значением

phsize equ ($-phdrs) ; аналогично рассчитываем размер program header

dd 2 ; p_type = PT_DYNAMIC
dd dynamic - $$ ; p_offset
dd dynamic ; p_vaddr
dd dynamic ; p_paddr
dd dynamic_size ; p_filesz
dd dynamic_size ; p_memsz
dd 6 ; p_flags = PF_RW
dd 4 ; p_align

dd 3 ; p_type = PT_INTERP
dd interp - $$ ; p_offset
dd interp ; p_vaddr
dd interp ; p_paddr
dd interp_size ; p_filesz
dd interp_size ; p_memsz
dd 4 ; p_flags = PF_R
dd 1 ; p_align

; данные для PT_DYNAMIC
; только самый необходимый минимум
dynamic:
dd 1, st_libdl_name ; DT_NEEDED -- необходимо прилинковать библиотеку с именем, лежащим в st_libdl_name (индекс в таблице символов)
; опциональные рюшечки, просящие интерпретатор заранее проверить, все ли необходимые библиотеки на месте
;dd 1, st_libSDL_name
;dd 1, st_libGL_name
dd 4, dt_hash ; DT_HASH -- указатель на таблицу хешей
dd 5, dt_strtab ; DT_STRTAB -- указатель на таблицу строк. все ссылки на строки имеют смысл смещения относительно начала таблицы строк
dd 6, dt_symtab ; DT_SYMTAB -- указатель на таблицу символов
dd 10, dt_strtab_size ; DT_STRSZ -- размер таблицы строк
dd 11, 16 ; DT_SYMENT -- размер одной записи в таблице символов
dd 17, dt_rel ; DT_REL -- указатель на таблицу релокаций
dd 18, dt_rel_size; DT_RELSZ -- размер таблицы релокаций
dd 19, 8 ; DT_RELENT -- размер одной записи в таблице релокаций
dd 0, 0 ; DT_NULL -- конец данных для DT_DYNAMIC
dynamic_size equ $ - dynamic

; данные для DT_HASH
; эти данные нужны для чего-то вроде оптимизации загрузки миллионов импортируемых 
; функций из динамических библиотек, однако ж для наших двух функций это оверкилл
; поэтому здесь просто заглушка
dt_hash: dd 1, 3, 0, 0, 0, 0

; данные для DT_SYMTAB
; именно в этом месте мы говорим, какие функции нам нужно подгрузить динамически
dt_symtab:
; 1 -- первая запись пустая (зачем?!)
dd 0, 0, 0
dw 0, 0 ; SHN_UNDEF
; 2 'dlopen'
dd st_dlopen_name, 0, 0
dw 0x12 ; = ELF32_ST_INFO(STB_GLOBAL, STT_FUNC), т.е., короче говоря, тип символа -- глобальная функция
dw 0 ; SHN_UNDEF говорит, что этого символа у нас нет, и его надо искать вовне
; 3 'dlsym'
dd st_dlsym_name, 0, 0
dw 0x12, 0 ; --//--

; данные для DT_REL
; таблица релокаций. описывает, куда и как загружать адреса символов из таблицы символов
dt_rel:
dd rel_dlopen ; адрес, куда грузить
dd 0x0101 ; ELF32_R_INFO(1,R_386_32) : dt_symtab[1] ('dlopen'), тип = запиши адрес символа + r_addend(=0 у нас)
dd rel_dlsym ; --//--
dd 0x0201 ; ELF32_R_INFO(2,R_386_32) : dt_symtab[2] ('dlsym'), --//--
dt_rel_size equ $ - dt_rel

; сгруппируем строки вмете -- ожидаем, что так они будут лучше паковаться
; стандартный линуксовый интерпретатор для динамической линковки
interp: db '/lib/ld-linux.so.2', 0
interp_size equ $ - interp

; данные для DT_STRTAB
; таблица строк. в ней лежит всё, необходимое для PT_DYNAMIC -- названия библиотек и имена функций
dt_strtab:
st_libdl_name equ $ - dt_strtab ; адрес строки относительно начала таблицы строк
db 'libdl.so.2', 0 ; все строки -- нуль-терминированные
st_dlopen_name equ $ - dt_strtab
db 'dlopen', 0
st_dlsym_name equ $ - dt_strtab
db 'dlsym', 0 
dt_strtab_size equ $ - dt_strtab

; имена библиотек и функций, которые нужно загрузить ручками
libs_to_dl:
st_libSDL_name equ $ - dt_strtab
db 'libSDL-1.2.so.0', 0 ; самое кросс-дистрибутивное название библиотеки, которое мне удалось выяснить эмпирически
db 'SDL_Init', 0
db 'SDL_SetVideoMode', 0
db 'SDL_PollEvent', 0
db 'SDL_GetTicks', 0
db 'SDL_ShowCursor', 0
db 'SDL_GL_SwapBuffers', 0
db 'SDL_Quit', 0
db 0 ; два нуля подряд = конец библиотеки
st_libGL_name equ $ - dt_strtab
db 'libGL.so.1', 0
db 'glViewport', 0
db 'glCreateShader', 0
db 'glShaderSource', 0
db 'glCompileShader', 0
db 'glCreateProgram', 0
db 'glAttachShader', 0
db 'glLinkProgram', 0
db 'glUseProgram', 0
db 'glRectf', 0
db 0, 0 ; три нуля подряд = конец загрузки

shader_vtx:
db 'varying vec4 p;'
db 'void main(){gl_Position=p=gl_Vertex;p.z=length(p.xy);}'
db 0

shader_frg:
db 'varying vec4 p;'
db 'void main(){'
db 'float '
db 'z=1./length(p.xy),'
db 'a=atan(p.x,p.y)+sin(p.z+z);'
db 'gl_FragColor='
db '2.*abs(.2*sin(p.z*3.+z*3.)+sin(p.z+a*4.)*p.xyxx*sin(vec4(z,a,a,a)))+(z-1.)*.1;'
db '}', 0

_start: ; поехали
 mov ebp, bss ; пускай ebp всё время указывает на bss -- будет удобно, поверьте!
; супер-удобные штуки, см далее
%define BSSADDR(a) ebp + ((a) - bss)
%define F(f) [ebp + ((f) - bss)]

; начнём загрузку функций
 mov esi, libs_to_dl+1 ; +1, т.к. ld_load ожидает, что мы уже вгрызлись на один символ в строку
 lea edi, [BSSADDR(libs_syms)] ; edi = адрес места, куда следует аккуратно сохранять адреса функций
ld_load:
 dec esi ; в этом месте мы уже вгрызлись в строку на 1, поэтому отступим назад
; подготовим параметры функции dlopen, они передаются через стек
 push 1 ; RTLD_LAZY
 push esi ; адрес имени библиотеки
 call F(rel_dlopen) ; eax = dlopen([esi], 1)
; обратите внимание, что здесь мы <s>не чистим после себя</s>засираем стек и РАДУЕМСЯ ЭТОМУ
 mov ebx, eax ; сохраним то, что dlopen нам вернул, в ebx

; скипаем все до 0
ld_skip_to_zero:
 lodsb
 test al, al
 jnz ld_skip_to_zero

; если следующий тоже \0 то конец текущей библиотеки
 lodsb
 test al, al
 jz ld_second_zero

 dec esi ; опять отматываемся на 1 назад
 push esi ; начало строки с названием функции
 push ebx ; возвращенный из dlopen указатель на загруженную библиотеку
 call F(rel_dlsym) ; eax = dlsym([ebx], [esi])
 stosd ; запишем eax (возвращенный указатель на функцию) в [edi], edi += 4
 jmp ld_skip_to_zero ; перемотаем до следуюшего нуля

ld_second_zero:
; если третий не ноль, то подгрузим что-нибудь еще!
 lodsb
 test al, al
 jnz ld_load

; здесь наша умопомрачительная интра!
 push 0x21 ; SDL_INIT_ TIMER | VIDEO
 call F(SDL_Init) ; SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO);

 push 2 | FULLSCREEN	; SDL_OPENGL
 push 32 ; 32 бита на пиксель
 push HEIGHT
 push WIDTH
 call F(SDL_SetVideoMode) ; SDL_SetVideoMode(WIDTH, HEIGHT, 32, SDL_OPENGL|FULLSCREEN);

; WxH уже есть в стеке! cdecl ftw!
 push 0
 push 0
 call F(glViewport) ; glViewport(0, 0, WIDTH, HEIGHT);
 call F(SDL_ShowCursor) ; SDL_ShowCursor(0);

; загрузка шейдеров
 call F(glCreateProgram) ; eax = glCreateProgram();
 mov edi, eax ; edi = program_id
 push 0x8b31
 pop esi ; esi = GL_VERTEX_SHADER
; здесь и далее используем 4 байта по адресу ebp как temp переменную -- там лежит ненужный нам более адрес dlopen, можно затирать
 mov dword [ebp], shader_vtx
 push esi
 call F(glCreateShader) ; eax = glCreateShader(GL_VERTEX_SHADER);
 mov ebx, eax
 push 0
 push ebp
 push 1
 push eax
 call F(glShaderSource) ; glShaderSource(shader_id, 1, &shader_vtx, 0);
 push ebx ; драйвера nVidia портят стек, поэтому нужно перезаливать аргументы
 call F(glCompileShader) ; glCompileShader(shader_id);
 push ebx ; опять поганая нвидия всё портит!
 push edi
 call F(glAttachShader) ; glAttachShader(program_id, shader_id);
 dec esi ; esi = GL_FRAGMENT_SHADER
 mov dword [ebp], shader_frg
; точная копия того, что вверху = хорошо жмётся!
 push esi
 call F(glCreateShader)
 mov ebx, eax
 push 0
 push ebp
 push 1
 push eax
 call F(glShaderSource)
 push ebx
 call F(glCompileShader)
 push ebx
 push edi
 call F(glAttachShader)
 push edi
 call  F(glLinkProgram) ; glLinkProgram(program_id);
 call  F(glUseProgram) ; glUseProgram(program_id);

mainloop:

 call F(SDL_GetTicks) ; eax == SDL_GetTicks(); -- время в миллисекундах
 mov [ebp], eax
 fninit ; нужно сбросить состояние FPU, потому что все вокруг норовят его испортить
 fild dword [ebp] ; st(0) = eax == time в миллисекундах, st(1) = 1000
 push 400 ; эта константа регулирует скорость, чем она больше, тем медленнее всё
 fild dword [esp] ; st(0) = 1000
 fdiv ; st(0) /= 1000 = время в секундах
 fld1 ; st(0) = 1, st(1) = время в с
 faddp st1 ; st(0) = время в с + 1
 fst dword [ebp]
 mov eax, [ebp] ; eax = (float-ieee)t в секундах
 fchs ; st(0) = -st(0)
 fstp dword [ebp]
 mov ebx, [ebp] ; ebx = -(float-ieee)t в секундах
 push ebx
 push ebx
 push eax
 push eax
 call F(glRectf) ; glRectf(-t,-t,t,t)
 times 5 pop eax ; здесь, к сожалению, приходится убирать за собой, т.к. мы крутимся в цикле и не можем гадить бесконечно

 call F(SDL_GL_SwapBuffers)

 lea edx, [BSSADDR(SDL_Event)] ; адрес памяти под структуру SDL_Event
 push edx
 call F(SDL_PollEvent) ; SDL_PollEvent(&SDL_Event);
 pop edx ; восстановим edx
 cmp byte [edx], 2 ; SDL_Event.type != SDL_KEYDOWN
 jnz mainloop

 call F(SDL_Quit) ; восстановим режим экрана и прочее
; вон из Новосибирска!
 xor eax, eax ; eax = 0
 inc eax ; ex = 1 (exit syscall)
 int 0x80 ; вызов syscall

file_size equ ($-$$) ; ну тут уже всё понятно должно быть

; BSS-секция, в ней лежат неинициализированные данные
absolute $
bss:

; резервируем место под адреса функций
libdl_syms:
rel_dlopen: resd 1
rel_dlsym: resd 1

libs_syms:
SDL_Init: resd 1
SDL_SetVideoMode: resd 1
SDL_PollEvent: resd 1
SDL_GetTicks: resd 1
SDL_ShowCursor: resd 1
SDL_GL_SwapBuffers: resd 1
SDL_Quit: resd 1
glViewport: resd 1
glCreateShader: resd 1
glShaderSource: resd 1
glCompileShader: resd 1
glCreateProgram: resd 1
glAttachShader: resd 1
glLinkProgram: resd 1
glUseProgram: resd 1
glRectf: resd 1

SDL_Event: resb 24

mem_size equ ($-$$)

Смотрите, котятки, мы уложили в 750 байт то, что в прошлый раз еле-еле влезло в 1024. Можно ли улучшить этот результат?
Конечно можно:
  1. многие структуры начинаются на то же самое, на что заканчиваются другие структуры
  2. похожие данные полезно размещать рядом (рыхлые заголовки вместе, строки — вместе, x86-инструкции — тоже ничем не перемежать)
  3. менять org
  4. выкидывать обходимое — например, закомментировать всё с SDL_ShowCursor

При этом стоит помнить, что размер сжатого файла далеко не монотонно зависит от размера несжатого: например, times 5 pop eax заметно выигрывает по размеру перед add esp, 20.
Итого, перенос всех строк в конец файла даёт выигрыш 16 байт, столько же можно выиграть, если закомментировать поганой метлой последние ненужные три поля elf header (e_shentsize и следующие за ним два гуся) поудалять нули и прочие одинаковые данные между phdrs и dynamic, dt_hash и dt_symtab.
Итого: 718 байт за то же самое.
Не знаю, как у вас, а у меня уже чешутся ручки — наконец-то можно заняться тем, зачем мы здесь все сегодня собрались — творчеством! И у нас есть целых 306 байт для него (даже больше, если учесть то, что можно полностью заменить шейдеры с осточертевшим туннелем)!
Что же можно сделать с таким невообразимо огромным холстом?

Например, что-нибудь такое





(осторожно, нужна мощная видеокарта):
shader_vtx:
db 'varying vec4 p,v;'
db 'void main()'
db '{'
db 'gl_Position=gl_Vertex;'
db 'p=vec4(mat3(cos(length(gl_Vertex.xy)),0.,sin(length(gl_Vertex.xy)),0.,1.,0.,-sin(length(gl_Vertex.xy)),0.,cos(length(gl_Vertex.xy)))*vec3(gl_Vertex.xy*.1,-.9),length(gl_Vertex.xy));'
db 'v=vec4(mat3(cos(length(gl_Vertex.xy)),0.,sin(length(gl_Vertex.xy)),0.,1.,0.,-sin(length(gl_Vertex.xy)),0.,cos(length(gl_Vertex.xy)))*vec3(gl_Vertex.xy*.1,.1),length(gl_Vertex.xy));'
db '}'
db 0

shader_frg:
db 'varying vec4 p,v;'
;db 'float mx(vec3 a){return max(a.x,max(a.y,a.z));}'
db 'float mn(vec3 a){return min(a.x,min(a.y,a.z));}'

db 'float F(vec3 a){return min(mn(vec3(1.)-abs(a)),-mn(abs(mod(a+vec3(.1),vec3(.4))-vec3(.2))-.15));}'
;db 'float F(vec3 a){return min(mn(vec3(1.)-abs(a)),length(mod(a,vec3(.4))-vec3(.2))-.06);}'

db 'vec3 n(vec3 a){'
db 'vec3 e=vec3(.0001,.0,.0);'
db 'return normalize(vec3(F(a)-F(a+e.xyy),F(a)-F(a+e.yxy),F(a)-F(a+e.yyx)));'
db '}'

db 'vec4 tr(vec3 E,vec3 D){'
db 'D=normalize(D);'
db 'float L=.01;'
db 'int i=0;'
db 'for(i;i<512;++i){'
db 'float d=F(E+D*L);'
db 'if(d<.0001)break;'
db 'L+=d;'
db '}'
;db 'return vec2(L,float(i)/512.);'
db 'return vec4(E+D*L,float(i)/512.);'
db '}'

db 'float I(vec3 a){'
db 'vec3 l=vec3(sin(p.w*1.3),cos(p.w*4.2),sin(p.w*3.2))*.9,la=l-a;'
db 'return length(tr(a,la).xyz-a)*dot(n(a),-normalize(la))/dot(la,la)+.01;'
;db 'return tr(a,-lv).x*F(a+lv)/dot(lv,lv)+.01;'
db '}'

db 'void main(){'
db 'vec4 t=tr(p.xyz,v.xyz);'
db 'gl_FragColor=I(t.xyz)*(abs(t)+vec4(t.w*5.));'
;db 'vec2 t=tr(p.xyz,v.xyz);'
;db 'vec3 q=p.xyz+normalize(v.xyz)*t.x;'
;db 'gl_FragColor=I(q)+vec4(t.y);'
db '}'
db 0


Или такое:


shader_vtx:
db 'varying vec4 p,v;'
db 'void main()'
db '{'
db 'gl_Position=gl_Vertex;'
db 'p=vec4(mat3(cos(length(gl_Vertex.xy)),0.,sin(length(gl_Vertex.xy)),0.,1.,0.,-sin(length(gl_Vertex.xy)),0.,cos(length(gl_Vertex.xy)))*vec3(gl_Vertex.xy*.1,-.9),length(gl_Vertex.xy));'
db 'v=vec4(mat3(cos(length(gl_Vertex.xy)),0.,sin(length(gl_Vertex.xy)),0.,1.,0.,-sin(length(gl_Vertex.xy)),0.,cos(length(gl_Vertex.xy)))*vec3(gl_Vertex.xy*.1,.1),length(gl_Vertex.xy));'
db '}'
db 0

shader_frg:
db 'varying vec4 p,v;'
db 'float mn(vec3 a){return min(a.x,min(a.y,a.z));}'

db 'float F(vec3 a){return min(mn(vec3(1.)-abs(a)),length(mod(a,vec3(.4))-vec3(.2))-.06);}'

db 'vec3 n(vec3 a){'
db 'vec3 e=vec3(.0001,.0,.0);'
db 'return normalize(vec3(F(a)-F(a+e.xyy),F(a)-F(a+e.yxy),F(a)-F(a+e.yyx)));'
db '}'

db 'vec3 tr(vec3 E,vec3 D){'
db 'D=normalize(D);'
db 'float L=.01;'
db 'int i=0;'
db 'for(i;i<512;++i){'
db 'float d=F(E+D*L);'
db 'if(d<.001)break;'
db 'L+=d;'
db '}'
db 'return E+D*L;'
db '}'

db 'vec3 I(vec3 a,vec3 l,vec3 c){'
db 'return c*(clamp(length(tr(a,l-a)-a),0.,length(l-a))*dot(n(a),normalize(a-l))/dot(l-a,l-a));'
db '}'

db 'void main(){'
db 'vec3 t=tr(p.xyz,v.xyz);'
db 'gl_FragColor=vec4('
db 'I(t,vec3(sin(p.w*1.3),cos(p.w*4.2),sin(p.w*3.2))*.7,vec3(.9,.6,.2))+'
db 'I(t,vec3(sin(p.w*3.2),sin(p.w*4.2),sin(p.w*1.3))*.7,vec3(.0,.3,.5)),1.);'
db '}'
db 0

Почему эти шейдеры дают такую картинку? Об этом я, следуя доброй традиции частоты постинга, расскажу в марте следующего года! Ключевые слова для нетерпеливых и самостоятельных: raymarching distance fields (много инфы есть у этого парня).

Эпилог

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

Несмотря на то, что с такими картинками нам уже не должно быть стыдно перед одноклассниками, и вообще мы уже должны слышать нарастающий гул каблуков и прочего визга поклонниц, развиваться всё ещё есть куда, и это ещё не конец, куда собрался, это ещё не конец!
В следующий раз вас ждёт рассказ о том, как на языке программирования «Си» доставать качающий бас из ничего и взрывать танцпол.
До встречи в октябре!
Tags:
Hubs:
+101
Comments 19
Comments Comments 19

Articles