Несколько подробностей о функции main
Однажды заинтересовался, содержимым стека функции main процесса в linux. Провел некоторые изыскания и теперь представляю вам результат.
Варианты описания функции main:
1. int main()
2. int main(int argc, char **argv)
3. int main(int argc, char **argv, char **env)
4. int main(int argc, char **argv, char **env, ElfW(auxv_t) auxv[])
5. int main(int argc, char **argv, char **env, char **apple)
argc — число параметров
argv — нуль-терминальный массив указателей на строки параметров командной строки
env — нуль-терминальный массив указателей на строки переменных окружения. Каждая строка в формате ИМЯ=ЗНАЧЕНИЕ
auxv — массив вспомогательных значение (доступно только для PowerPC [1])
apple — путь к исполняемому файлу (в MacOS и Darwin [2])
Вспомогательный вектор — массив с различной дополнительной информацией, такой как эффективный идентификатор пользователя, признак setuid бита, размер страницы памяти и т.п.
Далее о том как получить массив вспомогательных значений для i386 и x86_64, а также об остальном содержимом «сегмента» стека.
Размер сегмента стека можно глянуть в файле maps:
cat /proc/10918/maps
…
7ffffffa3000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
…
Перед тем, как загрузчик передаст управление в main, он инициализирует содержимое массивов параметров командной строки, переменных окружения, вспомогательный вектор.
После инициализации верхняя часть стека выглядит примерно так, для 64битной версии.
Старший адрес сверху.
1. | 0x7ffffffff000 | Верхняя точка сегмента стека. Обращение вызывает segfault | |||
0x7ffffffff0f8 | NULL | void* | 8 | 0x00′ | |
2. | filename[0] | char | 1+ | «/tmp/a.out» | |
char | 1 | 0x00 | |||
. | |||||
env[1][0] | char | 1 | 0x00 | ||
. | |||||
char | 1 | 0x00 | |||
3. | 0x7fffffffe5e0 | env[0][0] | char | 1 | .. |
char | 1 | 0x00 | |||
. | |||||
argv[1][0] | char | 1 | 0x00 | ||
. | |||||
char | 1 | 0x00 | |||
4. | 0x7fffffffe5be | argv[0][0] | char | 1+ | «/tmp/a.out» |
5. | Массив случайной длины | ||||
6. | данные для auxv | void*[] | 48′ | ||
AT_NULL | Elf64_auxv_t | 16 | |||
. | |||||
auxv[1] | Elf64_auxv_t | 16 | |||
7. | auxv[0] | Elf64_auxv_t | 16 | Ex.: | |
NULL | void* | 8 | 0x00 | ||
. | |||||
env[1] | char* | 8 | |||
8. | 0x7fffffffe308 | env[0] | char* | 8 | 0x7fffffffe5e0 |
NULL | void* | 8 | 0x00 | ||
. | |||||
argv[1] | char* | 8 | |||
9. | 0x7fffffffe2f8 | argv[0] | char* | 8 | 0x7fffffffe5be |
10. | 0x7fffffffe2f0 | argc | long int | 8′ | число аргументов + 1 |
11. | Локальные переменные и аргументы, функций вызываемых до main | ||||
12. | Локальные переменные main | ||||
13. | 0x7fffffffe1fc | argc | int | 4 | число аргументов + 1 |
0x7fffffffe1f0 | argv | char** | 8 | 0x7fffffffe2f8 | |
0x7fffffffe1e8 | env | char** | 8 | 0x7fffffffe308 | |
14. | Переменные локальных функций |
‘ — описания полей в документах не нашел, но в дампе явно видны.
Для 32 битов не проверял, но скорее всего достаточно только разделить размеры на два.
1. Обращение к адресам, выше верхней точки, вызывает Segfault.
2. Строка, содержащая путь к исполняемому файлу.
3. Массив строк с переменными окружения
4. Массив строк с параметрами командной строки
5. Массив случайной длинны. Его выделение можно отключить командами
sysctl -w kernel.randomize_va_space=0
echo 0 > /proc/sys/kernel/randomize_va_space
6. Данные для вспомогательного вектора (например строка «x86_64»)
7. Вспомогательный вектор. Подробнее ниже.
8. Нуль-терминальный массив указателей на строки переменных окружения
9. Нуль-терминальный массив указателей на строки параметров командной строки
10.Машинное слово, содержащее число параметров командной строки (один из аргументов «старших» функций см. п. 11)
11.Локальные переменные и аргументы, функций вызываемых до main(_start,__libc_start_main..)
12.Переменные, объявленные в main
13.Аргументы функции main
14.Переменные и аргументы локальных функций.
Вспомогательный вектор
Для i386 и x86_64 нельзя получить адрес первого элемента вспомогательного вектора, однако содержимое этого вектора можно получить другими способами. Один из них — обратиться к области памяти, лежащей сразу за массивом указателей на строки переменных окружения.
Это должно выглядеть примерно так:
#include #include int main(int argc, char** argv, char** env)< Elf64_auxv_t *auxv; //x86_64 // Elf32_auxv_t *auxv; //i386 while(*env++ != NULL); //ищем начало вспомогательного вектора for (auxv = (Elf64_auxv_t *)env; auxv->a_type != AT_NULL; auxv++)< printf("addr: %p type: %lx is: 0x%lx\n", auxv, auxv->a_type, auxv->a_un.a_val); > printf("\n (void*)(*argv) - (void*)auxv= %p - %p = %ld\n (void*)(argv)-(void*)(&auxv)=%p-%p = %ld\n ", (void*)(*argv), (void*)auxv, (void*)(*argv) - (void*)auxv, (void*)(argv), (void*)(&auxv), (void*)(argv) - (void*)(&auxv)); printf("\n argc copy: %d\n",*((int *)(argv - 1))); return 0; >
Структуры Elf_auxv_t описаны в /usr/include/elf.h. Функции заполнения структур в linux-kernel/fs/binfmt_elf.c
Второй способ получить содержимое вектора:
hexdump /proc/self/auxv
Самый удобочитаемое представление получается установкой переменной окружения LD_SHOW_AUXV.
LD_SHOW_AUXV=1 ls
AT_HWCAP: bfebfbff //возможности процессора
AT_PAGESZ: 4096 //размер страницы памяти
AT_CLKTCK: 100 //частота обновления times()
AT_PHDR: 0x400040 //информация о заголовке
AT_PHENT: 56
AT_PHNUM: 9
AT_BASE: 0x7fd00b5bc000 //адрес интерпретатора, то бишь ld.so
AT_FLAGS: 0x0
AT_ENTRY: 0x402490 //точка входа в программу
AT_UID: 1000 //идентификаторы пользователя и группы
AT_EUID: 1000 //номинальные и эффективные
AT_GID: 1000
AT_EGID: 1000
AT_SECURE: 0 //поднят ли setuid флаг
AT_RANDOM: 0x7fff30bdc809 //адрес 16 случайных байт,
генерируемых при запуске
AT_SYSINFO_EHDR: 0x7fff30bff000 //указатель на страницу, используемую для
//системных вызовов
AT_EXECFN: /bin/ls
AT_PLATFORM: x86_64
Слева — название переменной, справа значение. Все возможные названия переменных и их описание можно глянуть в файле elf.h. (константы с префиксом AT_)
Возвращение из main()
После инициализации контекста процесса управление передается не в main(), а в функцию _start().
main() вызывает уже из __libc_start_main. Эта последняя функция имеет интересную особенность — ей передается указатель на функцию, которая должна быть выполнена после main(). И указатель этот передается естественно через стек.
Вообще аргументы __libc_start_main имеют вид, согласно файла glibc-2.11/sysdeps/ia64/elf/start.S
/*
* Arguments for __libc_start_main:
* out0: main
* out1: argc
* out2: argv
* out3: init
* out4: fini //функция вызываемая после main
* out5: rtld_fini
* out6: stack_end
*/
Т.е. чтобы получить адрес указателя fini нужно сместиться на два машинных слова от последней локальной переменной main.
Вот что получилось(работоспособность зависит от версии компилятора):
#include void **ret; void *leave; void foo() < void (*boo)(void); //указатель на функцию printf("Stack rewrite!\n"); boo = (void (*)(void))leave; boo(); // fini() >int main(int argc, char *argv[], char *envp[]) < unsigned long int mark = 0xbfbfbfbfbfbfbfbf; //метка, от которой будем работать ret = (void**)(&mark+2); // извлекаем адрес, функции, вызываемой после завершения (fini) leave = *ret; // запоминаем *ret = (void*)foo; // перетираем return 0; // вызов функции foo() >
Надеюсь, было интересно.
Удач.
Спасибо пользователю Xeor за полезную наводку.