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

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

мне нужен этот код для Linux archecture 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 ответа

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

Вот минимальный пример программы (для x86 и x86_64), которая читает argc а также argv, печатает все значения в argv на стандартный вывод (разделенный символом новой строки) и завершается с помощью 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, ссылки на которые приведены выше.

Этот ответ только для 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 делает. Glibc-х ld.so не может использоваться с любой другой библиотекой C, кроме самого glibc; если вы пишете свою собственную библиотеку C и хотите поддерживать динамическое связывание, вам также необходимо написать свою собственную ld.so, (Да, это прискорбно; в идеале динамический компоновщик должен быть отдельным проектом разработки, и его полный интерфейс должен быть указан.)

В качестве быстрого и грязного хака вы можете сделать исполняемый файл с скомпилированной функцией 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,

Другие вопросы по тегам