Как получить значение аргументов, используя встроенную сборку в 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
,