Несколько подробностей о функции 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 {0,0}
...
auxv[1] Elf64_auxv_t 16
7. auxv[0] Elf64_auxv_t 16 Ex.: {0x0e,0x3e8}
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 <stdio.h> 
#include <elf.h> 
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{32,64}_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 <stdio.h>

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 за полезную наводку.

1. www.gelato.unsw.edu.au/IA64wiki/AuxiliaryVector
2. unixjunkie.blogspot.com/2006/02/char-apple-argument-vector.html
3. articles.manugarg.com/aboutelfauxiliaryvectors.html
4. www.phrack.org/issues.html?issue=58&id=5#article
5. unixforum.org/index.php?showtopic=94993&st=30
6. sources.redhat.com/ml/libc-alpha/2007-06/msg00108.html
7. linux-kernel/fs/binfmt_elf.c
8. /usr/include/elf.h
9. glibc-2.11/sysdeps/ia64/elf/libc-start.c
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 8

    +3
    Белые пятна — это, по всей видимости, локальные переменные и аргументы функций стартап-кода си-библиотеки.

    В случае libc это будут _start и __libc_start_main

    Платформенно-зависимая _start:
    x86_64
    i386
    и т.п. для других платформ

    и независимая часть в __libc_start_main:
    libc-start.c

    Возможно и gdb покажет больше с отладочной версией libc
      0
      Посмотрел, спасибо, там, как раз самое интересное оказывается :)
      Сделал небольшой стековерфло :). В тексте тоже подправлю.
      #include <stdio.h>
      
      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[]) {
         int a[123] = {0};
         unsigned long int mark = 0xbfbfbfbfbfbfbfbf; //метка, от которой будем работать
         ret = (void**)(&mark+2); // извлекаем адрес, функции, вызываемой после завершения (fini)
         leave = *ret;             // запоминаем  
         *ret = (void*)foo;       // перетираем
         return 0;                // вызов функции foo()
      }
      
        0
        сейчас проверил на другой машине — не работает, т.к. там другой порядок расположения переменных в стеке.
      +1
      Вместо Denwer читать, конечно же, Darwin.
        +2
        Возле заголовка должна быть пиктограмма редактирования статьи.
          0
          Спасибо, поправил в тексте.
        +2
        Очень правильная статья для дня программиста :)
          +1
          А еще валиден такой код
          int main[] = {1, 2, 3};

          Only users with full accounts can post comments. Log in, please.