C - печать аргументов без stdlibs - PullRequest
0 голосов
/ 02 июля 2018

Я только что написал программу на C, которая печатает аргумент командной строки без использования стандартной библиотеки или функции main(). Моя мотивация - просто любопытство и понимание того, как играть со встроенной сборкой. Я использую Ubuntu 17.10 x86_64 с общим ядром 4.13.0-39 и GCC 7.2.0.

Ниже приведен мой код, который я постарался прокомментировать настолько, насколько я понял. Функции print, print_1, my_exit и _start() требуются системой для запуска исполняемого файла. На самом деле, без _start() компоновщик выдаст предупреждение, и программа выйдет из строя.

Функции print и print_1 различны. Первый выводит строку на консоль, измеряя длину строки внутри. Вторая функция нуждается в длине строки, передаваемой в качестве аргумента. Функция my_exit() просто выходит из программы, возвращая требуемое значение, которое в моем случае является длиной строки или числом аргументов командной строки.

print_1 требует длину строки в качестве аргумента, поэтому символы подсчитываются с помощью цикла while(), а длина сохраняется в strLength. В этом случае все работает довольно хорошо.

Странные вещи случаются, когда я использую функцию print, которая измеряет длину строки внутри. Проще говоря, похоже, что эта функция каким-то образом изменяет строковый указатель так, чтобы он указывал на переменные окружения, которые должны быть следующим указателем, и вместо первого аргумента функция выводит "CLUTTER_IM_MODULE=xim", что является моей первой переменной окружения. Мой обходной путь - назначить *a на *b в следующей строке.

Я не смог найти объяснения в процедуре подсчета, но похоже, что он меняет мой строковый указатель.

unsigned long long print(char * str){
unsigned long long ret;
__asm__(
        "pushq %%rbx \n\t"
        "pushq %%rcx \n\t"                      //RBX and RCX to the stack for further restoration
        "movq %1, %%rdi \n\t"                   //pointer to string (char * str) into RDI for SCASB instruction
        "movq %%rdi, %%rbx \n\t"                //saving RDI in RBX for final substraction
        "xor %%al, %%al \n\t"                   //zeroing AL for SCASB comparing
        "movq $0xffffffff, %%rcx \n\t"          //max string length for REPNE instruction
        "repne scasb \n\t"                      //counting "loop"       see details: https://www.felixcloutier.com/x86/index.html   for REPNE and SCASB instructions
        "sub %%rbx, %%rdi \n\t"                 //final substraction
        "movq %%rdi, %%rdx \n\t"                //string length for write syscall
        "movq %%rdi, %0 \n\t"                   //string length into ret to return from print
        "popq %%rcx \n\t"
        "popq %%rbx \n\t"                       //RBX and RCX restoration

        "movq $1, %%rax \n\t"                   //write - 1 for syscall
        "movq $1, %%rdi \n\t"                   //destination pointer for string operations $1 - stdout
        "movq %1, %%rsi \n\t"                   //source string pointer
        "syscall \n\t"
        : "=g"(ret)
        : "g"(str)
        );
return ret; }

void print_1(char * str, int l){
int ret = 0;

__asm__("movq $1, %%rax \n\t"                   //write - 1 for syscall
        "movq $1, %%rdi \n\t"                   //destination pointer for string operations
        "movq %1, %%rsi \n\t"                   //source pointer for string operations
        "movl %2, %%edx \n\t"                   //string length
        "syscall"
        : "=g"(ret)
        : "g"(str), "g" (l));}


void my_exit(unsigned long long ex){
int ret = 0;
__asm__("movq $60, %%rax\n\t"               //syscall 60 - exit
        "movq %1, %%rdi\n\t"                //return value
        "syscall\n\t"
        "ret"
        : "=g"(ret)
        : "g"(ex)
);}

void _start(){

register int ac __asm__("%rsi");                        // in absence of main() argc seems to be placed in rsi register
//int acp = ac;
unsigned long long strLength;
if(ac > 1){
    register unsigned long long * arg __asm__("%rsp");  //argv array
    char * a = (void*)*(arg + 7);                       //pointer to argv[1]
    char * b = a;                                       //work around for print function
    /*version with print_1 and while() loop for counting
        unsigned long long strLength = 0;
        while(*(a + strLength)) strLength++;
        print_1(a, strLength);
        print_1("\n", 1);
    */
    strLength = print(b);
    print("\n");
}
//my_exit(acp);         //echo $?   prints argc
my_exit(strLength);     //echo $?   prints string length}

Ответы [ 2 ]

0 голосов
/ 31 июля 2018

Большое спасибо за каждое предложение в вашем ответе и комментариях, это было действительно полезно. Питер Кордес , спасибо за эту ссылку https://stackoverflow.com/a/50261819. Я использую этот код в качестве основы и, следуя вашим советам, пишу встроенный asm в глобальном масштабе. После пары дней осмотра и прочтения некоторой документации, наконец, вот код, делающий то, что я искал (проверка аргументов командной строки и переменных среды без stdlib).

Любые улучшения и советы приветствуются.

Он компилируется с помощью: gcc -Wall -o getArgs getArgs.c -nostdlib -nostartfiles -fno-ид-статический -s

Выполнить: ./getArgs -args Привет всем -envs

*** Environment variable ***
/bin/bash

*** Command line arguments ***
-args
Hello
everybody
-envs

Соглашения о вызовах: Приложения уровня пользователя используют в качестве целочисленных регистров для передачи последовательности *% rdi,% rsi,% rdx,% rcx,% r8 и% r9. Таким образом, в вызове функции мы должны иметь ex. печать (% rdi,% rsi,% rdx); * Интерфейс ядра использует% rdi,% rsi,% rdx,% r10,% r8 и% r9.

asm(
    ".global _start\n\t"
    "_start:\n\t"
    "   xorl %ebp,%ebp\n\t"                     // Clear the frame pointer. As ABI suggests
    "   movq 0(%rsp),%rdi\n\t"                  // argc
    "   lea 8(%rsp),%rsi\n\t"                   // argv = %rsp + 8
    "   lea   8(%rsp,%rdi,8), %rdx\n\t"         // pointer to environment variables     (8*(argc+1))(%rsp)  envp[0]
    "   call __main\n\t"                        // call main function
    "   movq %rax,%rdi\n\t"                     // main return code as an argument for exit syscall
    "   movl $60,%eax\n\t"                      // 60 = exit
    "   syscall\n\t");
asm(
    "print:\n\t"                    // thanks to the calling convention when we call our print we get:  int fd (%rdi), const void *buf (%rsi), unsigned count (%rdx)
    "   movq $1,%rax\n\t"         // 1 = write syscall on x86_64
    "   syscall\n\t"
    "   ret\n\t"
);



int print(int fd, const void *buf, unsigned count); //do not forget to declare function from inline assembly


unsigned strLen(const char *ch) {
    const char *ptr;
    for(ptr = ch; *ptr; ++ptr);             //ptr points to same place as ch, then looping until *ptr is not 0. If so, after substraction we get string length.
    return ptr-ch;      }                   //"When you substract two pointers, as long as they point into the same array, the result is the number of elements separating them"

char strCmp(const char * a, const char * b){
     char t = 0;
     int aLength = strLen(a);
     int bLength = strLen(b);
    if(aLength == bLength){
        for(int j = 0; j < aLength; j++){
            if(a[j] == b[j])
                t++;
        }
        if(t == aLength)
            return 1;
        else
            return 0;
    }else{
        return 0;
}}   //strCmp - comparing 2 strings up to the length of first string, returns 1 if equal and 0 if not

char * getEnv(char * env, char **envp){
     char * val;
     int valL = strLen(env);
     int k = 1;
//environment variables is null terminated array of strings, last array element is 0
while(*(envp + k)){
    char t = 0;
    for(val = *(envp + k); *val != 0x3d; ++val);        //counting up to 3d (=) //ascii hex of "=" is 0x3d
    int envpL = val - *(envp + k);                      //counting length of envp
    if(valL == envpL){
        for(int j = 0; j < valL; j++){
            if(*(*(envp + k) + j) == *(env + j)){
                t++;
            }
        }
        if(t == valL){
            return ++val;
        }
    }
    k++;
}

return "";}     //getEnv - looping through environment variables "envp" looking for "env", using strLen()


int __main(int argc, char **argv, char **envp) {
         char arg1 = 0, arg2 = 0; int length;                       //arg1, arg2 - flags for argv checking
          //arrays to compare with command line arguments
         char envs[6] = {0x2d, 0x65, 0x6e, 0x76, 0x73, 0x00};           //ascii hex of "-envs"      
         char args[6] = {0x2d, 0x61, 0x72, 0x67, 0x73, 0x00};   //ascii hex of "-args"      
         //first of all we check for control arguments
        for(int i = 1; i < argc; i++) {

              if(strCmp(*(argv + i), envs))
                        arg1 = 1;
              if(strCmp(*(argv + i), args))
                        arg2 = 1;
         }

       if(arg1){
           char * b = getEnv("SHELL", envp);          //we are looking for "SHELL"
           print(1, "*** Environment variable ***\n", 30);
           print(1, b, strLen(b));
           print(1, "\n", 1);
       }
       if(arg2){
           print(1, "\n", 1);
           print(1, "*** Command line arguments ***\n", 31);
           for(int i = 1; i < argc; i++) {
               length = strLen(*(argv + i));
               print(1, *(argv + i), length);
               print(1, "\n", 1);
            }
        }

       return argc; }//number of arguments
0 голосов
/ 02 июля 2018

char * a = (void*)*(arg + 7); - вещь, которая «случается с работой», если она вообще работает. Если вы не пишете __attribute__((naked)) функции, которые только используют встроенный asm, компилятор полностью определяет, как он распределяет стековую память. Похоже, что вы получаете rsp, хотя это не гарантируется для этого неподдерживаемого использования local-asm local. (Использование запрошенного регистра гарантируется только при использовании в качестве операнда для встроенного оператора asm.)

Если вы компилируете с отключенной оптимизацией, gcc зарезервирует слоты стека для локальных пользователей, поэтому char * b = a; заставит gcc корректировать RSP при вводе функции , поэтому ваш хак может изменить код gcc на gen соответствует жестко заданному смещению +7 (умноженному на 8 байт), которое вы указали в источнике.

При входе в _start содержимое стека: argc в (%rsp), argv[], начиная с 8(%rsp). Над завершающим указателем NULL для argv [] массив envp[] также находится в памяти стека. Вот почему вы получаете CLUTTER_IM_MODULE=xim, когда жестко закодированное смещение получает неправильный слот стека.

// in absence of main() argc seems to be placed in rsi register

Это, вероятно, осталось от динамического компоновщика (который выполняется в вашем процессе до _start). Если вы скомпилировали с gcc -static -nostdlib -fno-pie, ваша _start будет реальной точкой входа в процесс, достигаемой непосредственно из ядра, со всеми регистрами = 0 (кроме RSP). Обратите внимание, что ABI говорит неопределенный; Linux решает обнулить их, чтобы избежать утечки информации.

Вы можете написать void _start(){} в GNU C, который надежно работает с и без включенной оптимизации и работает по правильным причинам, без встроенного asm (но все еще зависит от соглашения о вызовах SysV ABI x86-64 и макета стека ввода процесса). Не требуется жесткого кодирования смещений, которые случаются в коде gen gcc. Как получить значение аргументов, используя встроенную сборку в C без Glibc? . Он использует такие вещи, как int argc = (int)__builtin_return_address(0);, потому что _start не является функцией: первое, что нужно в стеке, это argc, а не адрес возврата. Это не красиво и не рекомендуется, но, учитывая соглашение о вызовах, вы можете получить gcc для генерации кода, который знает, где что находится.


Ваш кодоблокатор регистрируется, не сообщая об этом компилятору. Все в этом коде неприятно, и нет никаких оснований ожидать, что какой-либо из них будет работать согласованно. И если это произойдет, это случайно и может сломаться с различными окружающими кода или параметров компилятора. Если вы хотите написать целые функции, сделайте это в автономном asm (или в встроенном asm в глобальной области видимости) и объявите прототип C, чтобы компилятор мог его вызвать.

Посмотрите на вывод gcc asm и посмотрите, что он сгенерировал вокруг вашего кода. (например, поместите ваш код в http://godbolt.org/).. Вы, вероятно, увидите его, используя регистры, которые вы засорили в вашем ассемблере. (Если вы не скомпилировали с отключенной оптимизацией, в этом случае он ничего не хранит в регистрах между операторами C, чтобы поддерживать согласованную отладку. Только сглаживание RSP или RBP может вызвать проблемы, другие встроенные ошибки asm clobber останутся незамеченными.) Но слипание красной зоны все равно будет проблемой.

См. Также https://stackoverflow.com/tags/inline-assembly/info для ссылок на руководства и учебные пособия.


Правильный способ использования встроенного asm (если есть правильный путь) - это, как правило, позволить компилятору делать как можно больше . Таким образом, чтобы сделать системный вызов write, вы должны делать все с ограничениями ввода / вывода, и единственной инструкцией внутри шаблона asm будет "syscall", как в этом хорошем примере my_write function: Как вызвать систему вызов через sysenter во встроенной сборке? (Фактический ответ имеет 32-разрядную int $0x80 и x86-64 syscall, но не встроенную версию asm с использованием 32-разрядной sysenter, поскольку это не гарантированно стабильный ABI ).

См. Также В чем разница между «asm», «__asm» и «__asm ​​__»? для другого примера.

https://gcc.gnu.org/wiki/DontUseInlineAsm по многим причинам, почему вы не должны его использовать (например, победить постоянное распространение и другие оптимизации).

Помните, что ограничение ввода указателя для встроенного оператора asm не подразумевает, что указанная память также является входом или выходом. Используйте "memory" clobber или см. at & t asm inline c ++ проблема для обхода обхода фиктивного операнда.

...