Написать пост

Обычно main — это функция. Но всегда ли это так?

Аватар Типичный программист

Обложка поста Обычно main — это функция. Но всегда ли это так?

Идея написать эту статью пришла мне в голову, когда одного из моих коллег заставили пройти начальный курс по 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?»

Следите за новыми постами
Следите за новыми постами по любимым темам
29К открытий29К показов