Почему эта программа распределяет следующие канареечные значения и ассемблерный код? - PullRequest
0 голосов
/ 01 мая 2019

Итак, в следующей программе intlen (), написанной в этой книге, когда я читаю язык ассемблера, соответствующий этой программе, intlen () предоставляет защищенное значение Canary, а также несколько значений, которые ВСЕ помещаются в стек.

Моя проблема с этим заключается в том, что в книге очень ясно сказано, что у вас есть шесть регистров, в которые можно поместить шесть переменных, и как только вы поместите эти переменные в регистры, и как только вы пройдете 6 регистров, ТОГДА все попадет в стек ,

Что мне нужно знать, так это то, почему программа intlen () помещает все свои значения в стек и понимает, почему канарейка помещается туда, где она есть.

Я уже пробовал гуглить в поиске ответа, а также подсчитывать переменные и аргументы в предыдущих программах, потому что «вызов» - это все-таки вещь, верно? Дело в том, что эти переменные в предыдущих программах увеличиваются до четырех.

Редактировать: Я также хотел бы знать, сколько len выделяется на указателе стека, когда защищено значением Canary. Вот как я думаю, что Лен работает. аргумент * s стоит 8 бит, защита стека - это еще 8 бит, так как мы находимся в 64-битной системе, а кадр стека при возврате составляет 8 бит, так что требуется всего 24 бита, верно?

/* C Code */ 
int len(char *s){
  return strlen(s);
}

void iptoa(char *s, long *p){
  long val = *p; 
  sprintf(s, "%ld", val); 
}

int intlen(long x){
  long v; 
  char buf[12]; 
  v = x; 
  iptoa(buf, &v); 
  return len(buf); 
}

===== аналог сборки =======

без защиты стека

1. intlen: 
2. subq  $40, %rsp 
3. movq %rdi, 24(rsp) 
4. leaq 24(%rsp), %rsi
5. movq %rsp, %rdi 
6. call iptoa 

с защитой

0. intlen:
1. subq $56, %rsp  
2. movq %fs:40, %rax  < Canary Value
3. movq %rax, 40(%rsp)  < Where the Canary goes (Why does this go here?) 
4. xorl %eax, %eax 
5. movq %rdi, 8(%rsp)
6. leaq 8(%rsp), %rsi
7. leaq 16(%rsp), %rdi 
8. call iptoa 

Я ожидаю, что большинство переменных будет в регистрах, но все, как вы можете видеть, помещается в указатель стека, и я пока не совсем понимаю, почему. Спасибо за ваше время.

Ответы [ 2 ]

1 голос
/ 01 мая 2019

Stack canary - это метод защиты от атак с разбиванием стека, которые, как правило, комментируют, если переполнение оставлено. Вот почему по умолчанию gcc вставит канареечные проверки, если функция имеет внутренний буфер, выделенный из стека.

Это можно отключить, используя -fno-stack-protector.

Также размер, который вызывает gcc для добавления канарейки, выбирается ssp-buffer-size.

Узнайте больше здесь

Что касается , почему локальные переменные хранятся в стеке? - ну, где бы вы еще их сохранили. Вы можете указать, что переменная будет оптимизирована как регистр с ключевым словом register, но это не гарантия. Количество ваших регистров ограничено, намного меньше, чем то, что может обрабатывать стек. Хранение их в регистрах оправдано только для оптимизации скорости.

0 голосов
/ 01 мая 2019

книга очень ясно показывает, что у вас есть шесть регистров, в которые можно поместить шесть переменных в

Вы читаете книгу о 32-битном x86.(И книга предполагает, что EBP будет использоваться как указатель кадра, оставляя только 6 из 8 целочисленных регистров как действительно универсальное).

Вы компилируете для x86-64 с включенной оптимизацией, которая включает -fomit-frame-pointerТаким образом, у вас есть 15 целочисленных регистров общего назначения.

Что мне нужно знать, так это то, почему программа [function] intlen() помещает все свои значения в стек

Это , а не , вполне то, что происходит.x остается в RDI, а не выливается в стек при входе в функцию, как если бы вы отключили оптимизацию (gcc -O0).Компиляция без оптимизации, чтобы увидеть большую разницу.

Компилятор поддерживает как можно большее количество переменных в regs, но v и buf должны существовать в памяти, потому что вы передаете указателиим не встроенная функция.

Вы, кажется, как-то отключили встраивание iptoa.Возможно, вы скомпилировали только с -O1, потому что у вас нет __attribute__((noinline)) в вашем определении iptoa.Если вы включили полную оптимизацию (-O3), вы увидите, что v оптимизируется, и вы просто получите movq %rdi, %rdx для передачи x в качестве 3-го аргумента sprintf.

Передача &v не встроенному iptoa означает, что память для v должна быть "синхронизирована" , потому что iptoa разрешено читать эту память через указатель, который вы ей передали.См. Также «escape анализ» - если указатель на переменную «экранирует» функцию, компилятор не может оптимизировать ее или делать с ней слишком много странных вещей.

IDK почемувы передаете целое число по ссылке;Вы написали код, который заставляет компилятор использовать память для большинства своих переменных.(Если он не может быть встроен.)


Кстати, вы знаете, что ваша функция очень неэффективна, верно? Вам не нужно вычислять каждую десятичную цифру с помощью sprintf , просто найдите первую степень 10, которая больше числа.

int intlen_fast(long x) {
    unsigned long absx = x;
    unsigned len = 1;      // even 0..9 takes 1 decimal digit
    if (x<0) {
        absx = -x;         // unsigned abs correctly handles the most-negative 2's complement integer
        len = 2;           // the minus sign
    }

    // don't need to check for overflow of pow10 with 64-bit integers
    // but in general we do to get the right count. (TODO)
    for (unsigned long pow10 = 10; pow10 <= absx ; pow10*=10) {
        len++;
    }
    return len;
}

Выполнение pow10 *= 10; значительно более эффективночем x /= 10, даже с оптимизированным делением на константу времени компиляции.

Для 64-битного unsigned long, это имеет очень хорошее свойство, что abs(LLONG_MIN) = 9223372036854775808ULL, и следующая наибольшая мощность 10 не делаетt переполнение unsigned long long.(ULLONG_MAX = 18446744073709551615ULL)

Если это не так (например, для 32-битных unsigned long в других ABI), вам необходимо проверить, является ли специальный случай absx >= 1000000000 для правильногообрабатывать входные величины в диапазоне от 1000000000 до 2147483648, потому что 2 ^ 32-1 = 4294967296.(К счастью, мы не получаем бесконечный цикл, просто 2 дополнительных итерации до тех пор, пока pow10 = 0xd4a51000 не станет без знака выше величины любого 32-разрядного целого числа со знаком. Но это все еще неправильный ответ!)В общем, C ++ имеет std::numeric_limits<long>::digits10 против std::numeric_limits<unsigned long>::digits10, что может быть полезно для определения во время компиляции, нужна ли нам дополнительная проверка.Или на самом деле нет, потому что он округляется до двоичного значения ширины в битах std::log10(2).

Может быть, проверка во время компиляции на основе Как округлить до ближайшей степени 10? LONG_MAX меньше, чем ULONG_MAX, если ваш компилятор может выполнять постоянное распространение через floor(log10(ULONG_MAX)).


Если вы не хотите беспокоиться о деталях pow10возможно переполнение, все равно было бы намного быстрее, чем вызывать sprintf, чтобы просто повторить деление на 10 для подсчета цифр.

Или, может быть, сделать одно деление на 10, а затем выполнить цикл pow10 вверх.Это было бы безопасно от переполнения / обтекания и просто.(Но вы все равно должны обрабатывать отрицательный ввод специально).


Но в любом случае оптимизированная версия из gcc8.3 -O3, конечно, сохраняет все свои переменные в регистрах ( компилятор Godboltисследователь ).-fstack-protector-strong не влияет на эту функцию, потому что она не имеет никаких массивов.

# gcc8.3 -O3 -fverbose-asm -fstack-protector-strong
intlen_fast(long):
        testq   %rdi, %rdi    # x
        js      .L14        #,
        movl    $1, %eax        #, <retval>
        movl    $1, %edx        #, len
.L15:
        cmpq    $9, %rdi        #, absx
        jbe     .L13      #,
        movl    $10, %eax       #, pow10
.L17:
        leaq    (%rax,%rax,4), %rax     #, tmp95    # pow10 * 5
        addl    $1, %edx        #, len
        addq    %rax, %rax      # pow10             # pow10 *= 10
        cmpq    %rax, %rdi      # pow10, absx
        jnb     .L17      #,
        movl    %edx, %eax      # len, <retval>
.L13:
        ret     
.L14:
        negq    %rdi    # absx
        movl    $2, %eax        #, <retval>
        movl    $2, %edx        #, len
        jmp     .L15      #

(похоже на пропущенную оптимизацию, что gcc устанавливает и EAX, и EDX. Он должен просто использовать RDX внутри цикла для pow10 и len в EAX.)

См. Ссылку на Godbolt длянекоторые тестовые вызовы, которые показывают, что это работает для угловых случаев, таких как -9, 99, 100 и 101, без ошибок, связанных с ошибкой.И для больших входов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...