Как получить значение аргументов, используя встроенную сборку в C без Glibc? - PullRequest
0 голосов
/ 09 мая 2018

Как получить значение аргументов, используя встроенную сборку в C без Glibc?

мне нужен этот код для Linux археологии x86_64 и i386. если вы знаете о MAC OS X или Windows, также отправьте и, пожалуйста, руководство.

void exit(int code)
{
    //This function not important!
    //...
}
void _start()
{
    //How Get arguments value using inline assembly
    //in C without Glibc?
    //argc
    //argv
    exit(0);
}

Новое обновление

https://gist.github.com/apsun/deccca33244471c1849d29cc6bb5c78e

и

#define ReadRdi(To) asm("movq %%rdi,%0" : "=r"(To));
#define ReadRsi(To) asm("movq %%rsi,%0" : "=r"(To));
long argcL;
long argvL;
ReadRdi(argcL);
ReadRsi(argvL);
int argc = (int) argcL;
//char **argv = (char **) argvL;
exit(argc);

Но все равно возвращается 0. Так что этот код неверен! пожалуйста, помогите.

Ответы [ 3 ]

0 голосов
/ 09 мая 2018

Как указано в комментарии, argc и argv предоставляются в стеке, поэтому вы не можете использовать обычную функцию C для их получения, даже при встроенной сборке, так как компилятор будет касаться указателя стека, чтобы выделить локальные переменные, настройка стека и т. д .; следовательно, _start должно быть записано на ассемблере, как это делается в glibc ( x86 ; x86_64 ). Можно написать небольшую заглушку, чтобы просто взять материал и переслать его на вашу «настоящую» точку входа C в соответствии с обычным соглашением о вызовах.

Здесь приведен минимальный пример программы (как для x86, так и для x86_64), которая читает argc и argv, печатает все значения в argv на stdout (разделенные символом новой строки) и завершает работу, используя argc в качестве состояния код; он может быть скомпилирован с обычным gcc -nostdlib-static, чтобы убедиться, что ld.so не задействован; не то, чтобы он здесь вредил).

#ifdef __x86_64__
asm(
        ".global _start\n"
        "_start:\n"
        "   xorl %ebp,%ebp\n"       // mark outermost stack frame
        "   movq 0(%rsp),%rdi\n"    // get argc
        "   lea 8(%rsp),%rsi\n"     // the arguments are pushed just below, so argv = %rbp + 8
        "   call bare_main\n"       // call our bare_main
        "   movq %rax,%rdi\n"       // take the main return code and use it as first argument for...
        "   movl $60,%eax\n"        // ... the exit syscall
        "   syscall\n"
        "   int3\n");               // just in case

asm(
        "bare_write:\n"             // write syscall wrapper; the calling convention is pretty much ok as is
        "   movq $1,%rax\n"         // 1 = write syscall on x86_64
        "   syscall\n"
        "   ret\n");
#endif
#ifdef __i386__
asm(
        ".global _start\n"
        "_start:\n"
        "   xorl %ebp,%ebp\n"       // mark outermost stack frame
        "   movl 0(%esp),%edi\n"    // argc is on the top of the stack
        "   lea 4(%esp),%esi\n"     // as above, but with 4-byte pointers
        "   sub $8,%esp\n"          // the start starts 16-byte aligned, we have to push 2*4 bytes; "waste" 8 bytes
        "   pushl %esi\n"           // to keep it aligned after pushing our arguments
        "   pushl %edi\n"
        "   call bare_main\n"       // call our bare_main
        "   add $8,%esp\n"          // fix the stack after call (actually useless here)
        "   movl %eax,%ebx\n"       // take the main return code and use it as first argument for...
        "   movl $1,%eax\n"         // ... the exit syscall
        "   int $0x80\n"
        "   int3\n");               // just in case

asm(
        "bare_write:\n"             // write syscall wrapper; convert the user-mode calling convention to the syscall convention
        "   pushl %ebx\n"           // ebx is callee-preserved
        "   movl 8(%esp),%ebx\n"    // just move stuff from the stack to the correct registers
        "   movl 12(%esp),%ecx\n"
        "   movl 16(%esp),%edx\n"
        "   mov $4,%eax\n"          // 4 = write syscall on i386
        "   int $0x80\n"
        "   popl %ebx\n"            // restore ebx
        "   ret\n");                // notice: the return value is already ok in %eax
#endif

int bare_write(int fd, const void *buf, unsigned count);

unsigned my_strlen(const char *ch) {
    const char *ptr;
    for(ptr = ch; *ptr; ++ptr);
    return ptr-ch;
}

int bare_main(int argc, char *argv[]) {
    for(int i = 0; i < argc; ++i) {
        int len = my_strlen(argv[i]);
        bare_write(1, argv[i], len);
        bare_write(1, "\n", 1);
    }
    return argc;
}

Обратите внимание, что здесь игнорируются некоторые тонкости, в частности, бит atexit. Вся документация о состоянии запуска для конкретной машины была извлечена из комментариев в двух файлах glibc, ссылки на которые приведены выше.

0 голосов
/ 11 мая 2018

В качестве быстрого и грязного хака , вы можете создать исполняемый файл с скомпилированной функцией C в качестве точки входа ELF. Просто убедитесь, что вы используете exit или _exit вместо возврата.

Если он динамически связан, вы можете по-прежнему использовать функции glibc в Linux (поскольку динамический компоновщик выполняет функции инициализации glibc). Не все системы такие, например, в cygwin вы определенно не можете вызывать функции libc, если вы (или стартовый код CRT) не вызвали функции инициализации libc в правильном порядке. Я не уверен, что даже гарантируется, что это работает в Linux, поэтому не зависите от него, за исключением экспериментов на вашей собственной системе.

Я использовал _start + _exit для создания статического исполняемого файла для микробенчмаркинга сгенерированного компилятором кода с меньшими накладными расходами при запуске для perf stat ./a.out. _exit можно использовать, даже если glibc не был инициализирован, или использовать встроенный asm для запуска xor %edi,%edi / mov $60, %eax / syscall (sys_exit (0) в Linux), поэтому вам не нужно даже статически связывать libc .


С еще более грязным хакерством вы можете получить доступ к argc и argv, зная ABI System V x86-64, для которого вы компилируете (см. Ответ @ zwol для цитаты из документа ABI), и Чем состояние запуска процесса отличается от соглашения о вызове функции:

  • argc - это адрес возврата для нормальной функции (на что указывает RSP). GNU C имеет встроенную функцию доступа к обратному адресу текущей функции (или для перемещения по стеку).
  • argv[0] - это место, где должен быть 7-й аргумент целого числа / указателя (первый аргумент стека , чуть выше адреса возврата). Это происходит / кажется, работает, чтобы взять его адрес и использовать его в качестве массива!

// Works only for the x86-64 SystemV ABI; only tested on Linux.
// DO NOT USE THIS EXCEPT FOR EXPERIMENTS ON YOUR OWN COMPUTER.

#include <stdio.h>
#include <stdlib.h>

// tell gcc *this* function is called with a misaligned RSP
__attribute__((force_align_arg_pointer))
void _start(int dummy1, int dummy2, int dummy3, int dummy4, int dummy5, int dummy6, // register args
        char *argv0) {

    int argc = (int)(long)__builtin_return_address(0);  // load (%rsp), casts to silence gcc warnings.
    char **argv = &argv0;

    printf("argc = %d, argv[argc-1] = %s\n", argc, argv[argc-1]);

    printf("%f\n", 1.234);  // segfaults if RSP is misaligned
    exit(0);
    //_exit(0);  // without flushing stdio buffers!
}

   # with a version without the FP printf
peter@volta:~/src/SO$ gcc -nostartfiles _start.c -o bare_start 
peter@volta:~/src/SO$ ./bare_start 
argc = 1, argv[argc-1] = ./bare_start
peter@volta:~/src/SO$ ./bare_start abc def hij
argc = 4, argv[argc-1] = hij
peter@volta:~/src/SO$ file bare_start
bare_start: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=af27c8416b31bb74628ef9eec51a8fc84e49550c, not stripped
 # I could have used  -fno-pie -no-pie to make a non-PIE executable

Работает с оптимизацией или без, с gcc7.3. Меня беспокоило, что без оптимизации адрес argv0 будет ниже rbp, куда он копирует arg, а не его первоначальное местоположение. Но, видимо, это работает.

gcc -nostartfiles связывает glibc, но не стартовые файлы CRT.

gcc -nostdlib пропускает обе библиотеки и файлы запуска CRT.

Очень мало из этого гарантировано для работы, но на практике он работает с текущей версией gcc на текущей x86-64 Linux и работал в прошлом в течение многих лет. Если он сломается, вы сохраните обе части. IDK, какие функции C нарушены, если пропустить код запуска CRT и просто полагаться на динамический компоновщик для запуска функций инициализации glibc. Кроме того, если взять адрес аргумента arg и получить доступ к указателям над ним, это UB, чтобы вы могли получить неработающий код-ген. gcc7.3 делает то, что вы ожидаете в этом случае.


gcc -mincoming-stack-boundary=3 (то есть 2 ^ 3 = 8 байт) - это еще один способ заставить gcc перестроить стек, потому что значение по умолчанию -mpreferred-stack-boundary=4 2 ^ 4 = 16 все еще остается в силе. Но это заставляет gcc предполагать недопустимый RSP для всех функций, а не только для _start, поэтому я заглянул в документы и нашел атрибут, предназначенный для 32- бит, когда ABI перешел от только требующего выравнивания стека 4 байта к текущему требованию выравнивания 16 байтов для ESP в 32-битном режиме.

Требование SysV ABI для 64-битного режима всегда было 16-байтовым выравниванием, но опции gcc позволяют создавать код, который не следует ABI.

// test call to a function the compiler can't inline
// to see if gcc emits extra code to re-align the stack

// like it would if we'd used -mincoming-stack-boundary=3 to assume *all* functions
// have only 8-byte (2^3) aligned RSP on entry, with the default -mpreferred-stack-boundary=4
void foo() {
    int i = 0;
    atoi(NULL);
}

С -mincoming-stack-boundary=3 мы получаем код выравнивания стека там, где он нам не нужен. Код перестановки стека в gcc довольно неуклюж, поэтому мы бы хотели этого избежать. (Не то, чтобы вы когда-либо использовали это для составления важной программы, в которой вы заботитесь об эффективности, используйте этот глупый компьютерный трюк только в качестве учебного эксперимента.)

Но в любом случае, см. Код в проводнике компилятора Godbolt с -mpreferred-stack-boundary=3.

и без него.
0 голосов
/ 09 мая 2018

Этот ответ только для x86-64, 64-битный Linux ABI. Все другие упомянутые ОС и ABI будут в целом схожи, но достаточно различны по мелким деталям, поэтому вам нужно будет написать свой пользовательский _start один раз для каждой.

Вы ищете спецификацию начального состояния процесса в " x86-64 psABI " или, чтобы дать ему полное название, "Двоичный интерфейс приложения System V" , Дополнение к архитектуре AMD64 (с моделями программирования LP64 и ILP32) ". Я воспроизведу рисунок 3.9, «Начальный стек процессов», здесь:

Purpose                            Start Address                  Length
------------------------------------------------------------------------
Information block, including                                      varies
argument strings, environment
strings, auxiliary information
...
------------------------------------------------------------------------
Null auxiliary vector entry                                  1 eightbyte
Auxiliary vector entries...                            2 eightbytes each
0                                                              eightbyte
Environment pointers...                                 1 eightbyte each
0                                  8+8*argc+%rsp               eightbyte
Argument pointers...               8+%rsp                argc eightbytes
Argument count                     %rsp                        eightbyte

Далее говорится, что начальные регистры не определены, кроме для %rsp, который, конечно, является указателем стека, и %rdx, который может содержать «указатель функции для регистрации в atexit».

Так что вся информация, которую вы ищете, уже присутствует в памяти, но она не была размещена в соответствии с обычным соглашением о вызовах, что означает, что вы должны написать _start на ассемблере. _start несет ответственность за настройку всего, чтобы вызывать main с учетом вышеизложенного. Минимальный _start будет выглядеть примерно так:

_start:
        xorl   %ebp, %ebp       #  mark the deepest stack frame

  # Current Linux doesn't pass an atexit function,
  # so you could leave out this part of what the ABI doc says you should do
  # You can't just keep the function pointer in a call-preserved register
  # and call it manually, even if you know the program won't call exit
  # directly, because atexit functions must be called in reverse order
  # of registration; this one, if it exists, is meant to be called last.
        testq  %rdx, %rdx       #  is there "a function pointer to
        je     skip_atexit      #  register with atexit"?

        movq   %rdx, %rdi       #  if so, do it
        call   atexit

skip_atexit:
        movq   (%rsp), %rdi           #  load argc
        leaq   8(%rsp), %rsi          #  calc argv (pointer to the array on the stack)
        leaq   8(%rsp,%rdi,8), %rdx   #  calc envp (starts after the NULL terminator for argv[])
        call   main

        movl   %eax, %edi   # pass return value of main to exit
        call   exit

        hlt                 # should never get here

(полностью не проверено.)

(Если вам интересно, почему нет настройки для поддержания выравнивания указателя стека, это потому, что при обычном вызове процедуры 8(%rsp) выравнивается по 16 байтам, но при вызове _start сама %rsp выровнен на 16 байт. Каждая команда call смещает %rsp на восемь, создавая ситуацию выравнивания, ожидаемую обычными скомпилированными функциями.)

Более тщательный _start будет делать больше вещей, таких как очистка всех других регистров, организация большего выравнивания указателя стека, чем по умолчанию, при желании, вызов собственных функций инициализации библиотеки C, настройка environ, инициализация состояние, используемое локальным хранилищем потока, создание чего-то конструктивного со вспомогательным вектором и т. д.

Вы также должны знать, что если есть динамический компоновщик (секция PT_INTERP в исполняемом файле), он получает управление раньше, чем _start. ld.so Glibc нельзя использовать с любой другой библиотекой C, кроме самого glibc; если вы пишете свою собственную библиотеку C и хотите поддерживать динамическое связывание, вам также нужно написать собственную ld.so. (Да, это прискорбно; в идеале динамический компоновщик должен быть отдельным проектом разработки, и его полный интерфейс должен быть указан.)

...