Обнаружение арифметического переполнения в C с помощью NASM

Этот фрагмент содержит информацию о том, установлен ли флаг переноса или очищен. Я работаю над Mac OSX, поэтому мой фрагмент поддерживает только Mac.

Во-первых, нам нужна процедура, которая выполняет задание:

func.s

global _read_carry_flag

_read_carry_flag:
    mov al, 0
    jnc end 
    mov al, 1
end:
    ret

(Попробуйте nasm2 -f macho64 func.s для компиляции в файл объекта.)

main.c

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>

#define A (2 * 1000 * 1000 * 1000)
#define B (1 * 1000 * 1000 * 1000)

extern bool read_carry_flag();

int main(int argc, char* argv[])
{
    int32_t a = A;
    int32_t b = B;
    int32_t ret = a + a + b;

    printf("%d\n", read_carry_flag());

    a = A;
    b = 1;
    ret = a + b;

    printf("%d\n", read_carry_flag());
    return 0;
}

(Попробуйте gcc -o prog main.c func.o для получения образа процесса.)

Я хотел бы услышать о возможных улучшениях /расширениях идеи.

11 голосов | спросил coderodde 6 Jpm1000000pmWed, 06 Jan 2016 20:57:27 +030016 2016, 20:57:27

4 ответа


12

Неверный флаг

Я считаю, что вы должны смотреть флаг переполнения вместо флага переноса, так как все ваши операнды являются значениями, подписанными. На x86 флаг переполнения устанавливается, если переполнение надписей дополнено. Флаг переноса устанавливается, если неподписанное дополнение переполняется.

Неверно

Как отметил @Edward, нет надежного использования этой функции, потому что вы никогда не знаете, как компилятор собирается изменить ваш код. Результаты не могут быть запутанными даже без компилятора, переупорядочивающего ваш код. Из вашего собственного примера:

ret = a + a + b;
overflow = read_overflow_flag();

Здесь вы обнаруживаете переполнение над вторым из двух дополнений. Если первое добавление переполнилось, а второе - нет, вы не поймаете его. Другими словами, сборка может выглядеть так:

add %ecx, %edx, %edx  // ret = a + a  <- overflow not detected here
add %ecx, %ecx, %ebx  // ret = ret + b
seto %al              // overflow = overflow flag

Чтобы сделать это правильно, я предлагаю вам использовать функцию, которая объединяет два числа и обновляет кумулятивное переполнение:

// If the add overflows, 1 will be added to *pOverflow.
int32_t add32_with_overflow(int32_t x, int32_t y, int *pOverflow);

int32_t ret      = 0;
int     overflow = 0;

ret = add32_with_overflow(a,   a, &overflow);
ret = add32_with_overflow(ret, b, &overflow);
printf("Overflow = %d\n", overflow);
ответил JS1 6 Jpm1000000pmWed, 06 Jan 2016 23:40:39 +030016 2016, 23:40:39
4

Его можно сделать на 2 байта короче, используя инструкцию ADC и избегая jmp (что также должно сделать это быстрее, но в этом случае нет особого смысла).

global _read_carry_flag

_read_carry_flag:
    mov al, 0
    adc al, 0
    ret

Как правильно указывает JS1 в комментарии, существует еще более короткий метод:

_read_carry_flag:
    setc al
    ret

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

ответил Edward 6 Jpm1000000pmWed, 06 Jan 2016 23:11:37 +030016 2016, 23:11:37
1

Возможное улучшение в read_carry_flag():

Может быть полезно либо xor eax зарегистрируйте себя перед перемещением возвращаемого значения в него или просто mov возвращаемое значение в eax вместо al, который обнуляет верхние 32 бита rax. Причина, по которой компилятор мог использовать регистр *ax до вызова вашей функции, и там все еще может быть значение, которое будет мешать возвращаемое значение вашей функции.

Вы передаете возвращаемое значение вашей функции непосредственно на printf(). В x86-64 ABI первые несколько интегральных аргументов передаются в 8-байтных регистрах, поэтому возвращаемое значение из read_carry_flag() будет передано в регистр rsi. Я не знаю, как именно компилятор переместит возвращаемое значение в регистр, но в соответствии с этим SO answer размер типа bool может быть больше одного байта.

Итак, если, например, sizeof bool == 2, то компилятор может сделать:

call _read_carry_flags
mov rdi, formatString
mov si, ax
mov eax, 0
call printf

(Команда mov eax, 0 требуется для сигнала printf(), что дальнейшие аргументы не передаются в стеке)

Если ax имеет значение 0x100 до того, как ваша функция была вызвана, а ваша функция выполнена

mov al, 0

После того, как ваша функция была вызвана, ax все равно будет содержать значение 0x100, и это будет передано в printf() вместо 0.

ответил Chase 7 Jam1000000amThu, 07 Jan 2016 04:16:58 +030016 2016, 04:16:58
1

При добавлении 2 32-битных номеров и попытке обнаружения переполнения код должен быть профилирован против самой прямой альтернативы. Оптимизирующий компилятор часто делает код приемлемым для работы (и намного проще в обслуживании).

int32_t a = A;
int32_t b = B;

// int32_t ret = a + a + b;
// printf("%d\n", read_carry_flag());

int64_t ret = (int64_t) a + a + b;
printf("%d\n", ret < INT32_MIN || ret > INT32_MAX);

Иначе, не обращаясь к более широким типам, код может использовать весьма портативный интерфейс:

// Does a+b overflow?
if (a >= 0) {
  if (b > INT32_MAX - a) Over();
} else {
  if (b < INT32_MIN - a) Under();
}
sum = a + b;
ответил chux 8 Jam1000000amFri, 08 Jan 2016 01:08:23 +030016 2016, 01:08:23

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132