Идея написать эту статью пришла мне в голову, когда одного из моих коллег заставили пройти начальный курс по CS в моем университете. Мы с ним искали способ написать корректную программу так, чтобы она проходила тесты, но ни один из экзаменующих не мог понять, как и почему она работает. Так я начал вспоминать разные трюки в C, которые видел когда-то в старом коде, и один из них был весьма занимательным. Идея этого трюка пришла мне в голову из-за названия блога «main is usually a function». Я подумал тогда: «А в каких случаях main
может не быть функцией?» Давайте это выясним!
Если вы хотите скачать исходный код к этой статье, я выложил его здесь. Обратите внимание, что я писал его под 64-битный Linux, и вам, возможно, придется поправить его под свою платформу.
Я, как и, полагаю, многие разработчики, ищу ответ на вопрос следующим образом. Шаг 1: поиск в Google. Шаг 2: перехожу по каждой релевантной ссылке на первой странице. Если проблема не решена, я изменяю запрос и повторяю все заново. В этот раз мне повезло, и ответ нашелся при первом же поиске на Stackoverflow. В 1984 году короткая программа выиграла IOCCC. main
в ней была объявлена так: short main[] = {...}
, и это каким-то образом работало! К сожалению, она была написана под абсолютно другую архитектуру и компилятор, и я не смог её собрать, чтобы посмотреть, что она делает, но, судя по тому, что это был просто набор чисел, я мог предположить, что в массиве были байты скомпилированного кода, который просто помещался в память на место main
.
Приняв версию, что код этой программы — скомпилированная функция main
, представленная в виде массива, давайте посмотрим, сможем ли мы написав маленькую программу и повторить этот трюк.
char main[] = "Hello world!";
$ gcc -Wall main_char.c -o first
main_char.c:1:6: warning: ‘main’ is usually a function [-Wmain]
char main[] = "Hello world!";
^
$ ./first
Segmentation fault
Отлично! Это сработало! Почти… Итак, наша следующая цель — напечатать что-нибудь на экран. Насколько я помнил тогда ассемблер, в скомпилированном коде есть секция команд и секция данных. При этом в секции команд содержится исполняемый, но не изменяемый код, а в секции данных лежат изменяемые данные, которые нельзя выполнить. В нашем случае, мы можем только заполнить код функции main
, поэтому все, что мы положим в секцию данных, будет нам недоступно. Нам нужно найти способ положить строку «Hello world!» в функцию main
и сослаться на нее.
Я стал думать, что можно написать за как можно меньшее количество строк. Поскольку я знал, что собираю программу под 64-битный Linux, я мог вызвать системную команду write
, которая выведет что-нибудь на экран. Сейчас я, конечно, понимаю, что мог тогда и не использовать ассемблер, но, в то же время, я рад, что получил такой опыт. Начинать со встроенного ассемблера в GCC было непросто, но когда я более-менее привык, дела пошли быстрее.
Сначала было очень трудно. Оказалось, что все, что я мог узнать об ассемблере через поисковик, это старый Intel-овский синтаксис, причем для 32-битной архитектуры. Мне же надо было скомпилировать код под 64-битную систему без каких-либо специальных флагов компилятора. Это значит никаких флагов и опций компилятора, никаких дополнительных шагов линковщика и встроенный в GCC синтаксис AT&T. Бо́льшую часть времени я потратил на поиск информации об ассемблере для 64-битных систем! Возможно, плохо искал. Здесь я пользовался по большей части методом проб и ошибок. Я всего лишь хотел вывести строку «Hello world!» на экран с помощью встроенного ассемблера, почему это так сложно? Для тех, кто хочет узнать, как это сделать, рекомендую взглянуть на эти сайты: Linux syscall list, Intro to Inline Asm, Differences between Intel and AT&T Syntax.
В конце концов, у меня начал получатся более-менее внятный asm-код, который даже работал. Вспомним, моя цель в том, чтобы написать такую main, которая представляет из себя массив с asm-командами, выводящими на экран «Hello World».
void main() {
__asm__ (
// print Hello World
"movl $1, %eax;\n" /* 1 is the syscall number for write on 64bit */
"movl $1, %ebx;\n" /* 1 is stdout and is the first argument */
"movl $message, %esi;\n" /* load the address of string into the second argument*/
"movl $13, %edx;\n" /* third argument is the length of the string to print*/
"syscall;\n"
// call exit (so it doesn't try to run the string Hello World)
// maybe I could have just used ret instead?
"movl $60,%eax;\n"
"xorl %ebx,%ebx; \n"
"syscall;\n"
// Store the Hello World inside the main function
"message: .ascii \"Hello World!\\n\";"
);
}
$ gcc -Wall asm_main.c -o second
asm_main.c:1:6: warning: return type of ‘main’ is not ‘int’ [-Wmain]
void main() {
^
$ ./second
Hello World!
Ура! Оно работает! Давайте посмотрим на скомпилированный код в 16-ричном виде, он должен совпадать один в один с ассемблерным листингом, который мы написали. Комментарии справа поясняют, что происходит.
(gdb) disass main
Dump of assembler code for function main:
0x00000000004004ed <+0>: push %rbp ; Compiler inserted
0x00000000004004ee <+1>: mov %rsp,%rbp
0x00000000004004f1 <+4>: mov $0x1,%eax ; Its our code!
0x00000000004004f6 <+9>: mov $0x1,%ebx
0x00000000004004fb <+14>: mov $0x400510,%esi
0x0000000000400500 <+19>: mov $0xd,%edx
0x0000000000400505 <+24>: syscall
0x0000000000400507 <+26>: mov $0x3c,%eax
0x000000000040050c <+31>: xor %ebx,%ebx
0x000000000040050e <+33>: syscall
0x0000000000400510 <+35>: rex.W ; String hello world
0x0000000000400511 <+36>: gs ; its garbled since
0x0000000000400512 <+37>: insb (%dx),%es:(%rdi) ; its not real asm
0x0000000000400513 <+38>: insb (%dx),%es:(%rdi) ; so it couldn't be
0x0000000000400514 <+39>: outsl %ds:(%rsi),(%dx) ; disassembled
0x0000000000400515 <+40>: and %dl,0x6f(%rdi)
0x0000000000400518 <+43>: jb 0x400586
0x000000000040051a <+45>: and %ecx,%fs:(%rdx)
0x000000000040051d <+48>: pop %rbp ; Compiler inserted
0x000000000040051e <+49>: retq
End of assembler dump.
Это на самом деле похоже на работающую main. Теперь давайте сделаем дамп её содержимого в виде строки 16-ричных символов и посмотрим работает ли она. Для этого также можно использовать gdb. Наверняка есть и какой-то более удобный способ, если вы его знаете, можете скинуть в комментарии к оригинальной статье. Когда мы ранее дизассемблировали main, её длина была 49 байт, т.е. мы можем использовать команду dump
для сохранения в файл в указанном далее виде.
# example of how to print the hex
(gdb) x/49xb main
0x4004ed <main>: 0x55 0x48 0x89 0xe5 0xb8 0x01 0x00 0x00
0x4004f5 <main+8>: 0x00 0xbb 0x01 0x00 0x00 0x00 0xbe 0x10
0x4004fd <main+16>: 0x05 0x40 0x00 0xba 0x0d 0x00 0x00 0x00
0x400505 <main+24>: 0x0f 0x05 0xb8 0x3c 0x00 0x00 0x00 0x31
0x40050d <main+32>: 0xdb 0x0f 0x05 0x48 0x65 0x6c 0x6c 0x6f
0x400515 <main+40>: 0x20 0x57 0x6f 0x72 0x6c 0x64 0x21 0x0a
0x40051d <main+48>: 0x5d
# example of how to save it to a file
(gdb) dump memory hex.out main main+49
Теперь у нас есть дамп, и мы можем сконвертировать его в обычные 10-ричные числа. Самый простой способ для этого, который я знаю — это использовать python. В python 2.6 и 2.7 можно просто использовать вот такую команду, чтобы получить подходящий в нашем случае массив целых чисел.
>>> import array
>>> hex_string = "554889E5B801000000BB01000000BE10054000BA0D0000000F05B83C00000031DB0F0548656C6C6F20576F726C64210A5D".decode("hex")
>>> array.array('B', hex_string)
array('B', [85, 72, 137, 229, 184, 1, 0, 0, 0, 187, 1, 0, 0, 0, 190, 16, 5, 64, 0, 186, 13, 0, 0, 0, 15, 5, 184, 60, 0, 0, 0, 49, 219, 15, 5, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 10, 93])
Я полагаю, если бы мои знания bash и unix были на более высоком уровне, я бы нашёл способ сделать это несколько проще, но гугл в ответ на что-нибудь вроде «hex dump of compiled function» выдаёт несколько вопросов про то, как напечатать 16-ричный дамп на разных языках. Тем не менее, у нас теперь есть массив разделённых запятыми чисел, представляющих нашу функцию. Попробуем записать его в новый файл и проверим, сработает ли трюк. Вот что получилось.
char main[] = {
85, // push %rbp
72, 137, 229, // mov %rsp,%rbp
184, 1, 0, 0, 0, // mov $0x1,%eax
187, 1, 0, 0, 0, // mov $0x1,%ebx
190, 16, 5, 64, 0, // mov $0x400510,%esi
186, 13, 0, 0, 0, // mov $0xd,%edx
15, 5, // syscall
184, 60, 0, 0, 0, // mov $0x3c,%eax
49, 219, // xor %ebx,%ebx
15, 5, // syscall
// Hello world!\n
72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100,
33, 10, // pop %rbp
93 // retq
};
$ gcc -Wall compiled_array_main.c -o third
compiled_array_main.c:1:6: warning: ‘main’ is usually a function [-Wmain]
char main[] = {
^
$ ./third
Segmentation fault
Segmentation fault! Что же я сделал не так? Настало время снова запустить gdb и попробовать посмотреть, в чём ошибка. Т.к. main теперь уже не функция, мы не можем просто использовать break main
, чтобы поставить точку останова. Вместо этого можно сделать break _start
, и мы получим останов на функции, которая передаёт управление на точку входа в libc (которая, в свою очередь, вызывает main) и сможем увидеть адрес, который передаётся в __libc_start_main
.
$ gdb ./third
(gdb) break _start
(gdb) run
(gdb) layout asm
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
B+>│0x400400 <_start> xor %ebp,%ebp │
│0x400402 <_start+2> mov %rdx,%r9 │
│0x400405 <_start+5> pop %rsi │
│0x400406 <_start+6> mov %rsp,%rdx │
│0x400409 <_start+9> and $0xfffffffffffffff0,%rsp │
│0x40040d <_start+13> push %rax │
│0x40040e <_start+14> push %rsp │
│0x40040f <_start+15> mov $0x400560,%r8 │
│0x400416 <_start+22> mov $0x4004f0,%rcx │
│0x40041d <_start+29> mov $0x601060,%rdi │
│0x400424 <_start+36> callq 0x4003e0 <__libc_start_main@plt> │
Я провел эксперимент, и понял, что в %rdi
помещается адрес main
, однако в этот раз что-то было не так. Ну конечно! Компилятор поместил main
в секцию данных! Как я уже отмечал, в секции данных хранятся изменяемые данные, но их нельзя выполнить, а в секции с кодом хранятся выполнимые инструкции, которые нельзя изменить. Код пытался выполнить инструкции из секции данных, что и вызывало segfault. И как мне было объяснить компилятору, что моя «main» должна располагаться в разделе с кодом? Поиск ничего не дал, и я был уверен, что это конец. Пора было сдаваться и заканчивать приключение.
Однако эта проблема не давала мне уснуть всю ночь. Я продолжил искать пока не нашел очевидное и простое решение на Stackoverflow, но, к сожалению, потерял на него ссылку. Все, что надо было сделать — объявить main
как const
. Я поменял объявление на const char main[] = {
и он расположился в правильной секции, и я заново попробовал его скомпилировать.
$ gcc -Wall const_array_main.c -o fourth
const_array_main.c:1:12: warning: ‘main’ is usually a function [-Wmain]
const char main[] = {
^
$ ./fourth
SL)�1�H��H�
А! Что он делает сейчас?! Время снова запускать gdb и понять, что происходит
gdb ./fourth
(gdb) break _start
(gdb) run
(gdb) layout asm
Глядя на код, мы видим, что адрес main (в ASM обозначается _start
) в инструкциях на моей машине выглядит так: mov $0x4005a0,%rdi
. Мы можем использовать это, чтобы поставить точку останова на main, выполняя break *0x4005a0
и затем продолжая выполнения с помощью c
.
(gdb) break *0x4005a0
(gdb) c
(gdb) x/49i $pc # $pc is the current executing instruction
...
0x4005a4 <main+4>: mov $0x1,%eax
0x4005a9 <main+9>: mov $0x1,%ebx
0x4005ae <main+14>: mov $0x400510,%esi
0x4005b3 <main+19>: mov $0xd,%edx
0x4005b8 <main+24>: syscall
...
Я вырезал часть кода, которая была не важна. Если вы не заметили причину ошибки, то она была в том, что адрес для вывода (0x400510
) не совпадал с адресом хранения строки «Hello world!\n» (0x4005c3
)! На самом деле он до сих пор указывает на вычисленный адрес в исходном исполняемом файле и не использует относительную адресацию для вывода. Это значит, что нам надо изменить ассемблерный код, чтобы загрузить адрес строки, относительный к текущему адресу. В данных условиях это довольно сложно выполнить в 32-битном коде, но к счастью мы используем 64-битный asm, так что можно использовать инструкцию lea
для упрощения задачи.
void main() {
__asm__ (
// print Hello World
"movl $1, %eax;\n" /* 1 is the syscall number for write */
"movl $1, %ebx;\n" /* 1 is stdout and is the first argument */
// "movl $message, %esi;\n" /* load the address of string into the second argument*/
// instead use this to load the address of the string
// as 16 bytes from the current instruction
"leal 16(%eip), %esi;\n"
"movl $13, %edx;\n" /* third argument is the length of the string to print*/
"syscall;\n"
// call exit (so it doesn't try to run the string Hello World
// maybe I could have just used ret instead
"movl $60,%eax;\n"
"xorl %ebx,%ebx; \n"
"syscall;\n"
// Store the Hello World inside the main function
"message: .ascii \"Hello World!\\n\";"
);
}
Измененный код прокомментирован, так что вы можете посмотреть его. Компилируем код и проверяем, что он работает.
$ gcc -Wall relative_str_asm.c -o fifth
relative_str_asm.c:1:6: warning: return type of ‘main’ is not ‘int’ [-Wmain]
void main() {
^
$ ./fifth
Hello World!
А сейчас мы можем снова использовать способы, описанные выше, чтобы извлечь hex-значения в виде целочисленного массива. Но в то же время, я хочу сделать это более скрытно и запутанно, используя все 4 байта, которые дают мне int’ы. Этого можно добиться, выводя информацию в gdb как int вместо выгрузки hex в файл и после копируя его в программу.
gdb ./fifth
(gdb) x/13dw main
0x4004ed <main>: -443987883 440 113408 -1922629632
0x4004fd <main+16>: 4149 899584 84869120 15544
0x40050d <main+32>: 266023168 1818576901 1461743468 1684828783
0x40051d <main+48>: -1017312735
Я выбрал число 13, так как размер main равен 49 байтам и результат деления 49 на 4 округляется до 13 для обеспечения надежности. Так как мы выходим из функции раньше, это ничего не меняет. Теперь, все, что нам необходимо сделать, — скопировать и вставить этот код в наш compiled_array_main.c
и запустить его.
const int main[] = {
-443987883, 440, 113408, -1922629632,
4149, 899584, 84869120, 15544,
266023168, 1818576901, 1461743468, 1684828783,
-1017312735
};
$ gcc -Wall final_array.c -o sixth
final_array.c:1:11: warning: ‘main’ is usually a function [-Wmain]
const int main[] = {
^
$ ./sixth
Hello World!
Всё это время мы игнорируем предупреждение о том, что main не является функцией.
Подозреваю, что все, что сделает комиссия, когда мой коллега покажет такой код на экзамене, — отругает за плохой стиль.
Перевод статьи «Main is usually a function. So then when is it not?»