-m32 дает необъяснимую проблему, когда аргумент без знака длинный длинный

учитывая следующий фрагмент кода с неправильным оператором printf для аргумента «a»:

#include <stdio.h>
void call(unsigned long long a, int b)
{
    printf("%lu,%d\n",a,b);
    printf("%llu,%d\n",a,b);
}
void main()
{
    call(0,1);
}

Когда вы скомпилируете это нормально, вы получите:

$ gcc m32.c
m32.c: In function ‘call’:
m32.c:4:12: warning: format ‘%lu’ expects argument of type ‘long unsigned int’, but argument 2 has type ‘long long unsigned int’ [-Wformat=]
     printf("%lu,%d\n",a,b);
            ^
$ ./a.out
0,1
0,1

но когда вы скомпилируете это с -m32, вы получите следующий вывод:

$ gcc -m32 m32.c
m32.c: In function ‘call’:
m32.c:4:12: warning: format ‘%lu’ expects argument of type ‘long unsigned int’, but argument 2 has type ‘long long unsigned int’ [-Wformat=]
     printf("%lu,%d\n",a,b);
            ^
$ ./a.out
0,0
0,1

Очевидно, что первый printf неверный, но как видите, после printf неверный второй аргумент в printf, хотя я и не ожидал, что это тоже произойдет. И я не могу этого объяснить. Как это возможно?


person Jan Heylen    schedule 10.04.2017    source источник
comment
Добавление флага -m32 в gcc не меняет тип типа данных.   -  person Gaurav Pathak    schedule 10.04.2017
comment
Вы уже знаете, что передаете неправильный тип параметра. Вы получаете предупреждение компилятора. Итак, почему вы удивляетесь, что программа вызывает неопределенное поведение?   -  person too honest for this site    schedule 10.04.2017
comment
@GauravPathak: Нет, типы данных не меняются! Спецификатор формата printf неверен для типов, переданных для любой цели. УБ есть УБ. И ОП, видимо, знает об этом.   -  person too honest for this site    schedule 10.04.2017
comment
В равной степени допустимым выводом будет Get your stuff together, Jan. We've been working together for who knows how long and you're still writing UB? I thought you were better than this!. Это УБ. Это не должно иметь смысла, потому что по определению не будет.   -  person Fund Monica's Lawsuit    schedule 11.04.2017


Ответы (3)


Хороший ответ @Attie! Кроме того, потому что вы вызвали мою низкоуровневую страсть :) Я попытаюсь дать другой взгляд на ответ.

Как вы, возможно, знаете, в архитектуре x86 аргументы функций передаются через стек, а в архитектуре x64 аргументы функции отправляются через регистры (RDI, RSI, RDX, RCX, R8, R9, именно в таком порядке).

Итак, что важно в связи с вашей проблемой, так это то, что при компиляции для 32 бит аргументы для вызовов printf отправляются через стек. Вот как выглядит ваш стек перед каждым из двух вызовов printf:

складывать перед вызовом printf

Каждый прямоугольный блок из стека представлен 32-битным числом (потому что вы находитесь в архитектуре x86). Вы хотите отправить 64-битное число в качестве первого аргумента для printf! Для этого компилятор разбивает длинное число без знака на две 32-битные части и помещает их в стек по отдельности. Вот почему вы получаете два нуля в стеке вместе с одним значением из целого числа.

Теперь давайте проанализируем первый вызов printf.

0,0

Поскольку он имеет спецификатор формата "%lu,%d\n", он должен брать из стека один unsigned long и один int. %lu в архитектуре x86 — это 32 бита, поэтому printf берет только один блок из стека. После этого берется еще один блок для целого числа (поскольку мы только «потребили» один из двух нулей с %lu, мы получим другой ноль для %d).

Второй вызов printf выводит нормальные значения.

0,1

Этот вызов выполняется со спецификатором формата "%llu,%d\n". В архитектуре x86 %llu является 64-битным, поэтому printf берет ДВА блока из стека, печатая, таким образом, ноль. После этого он берет из стека еще один блок для целого числа (это блок с единичным значением).

Вы должны быть очень осторожны со спецификатором формата строки, который вы отправляете в функцию printf! атака форматной строки – это хорошо известный тип атаки, основанный на таких проблемах, как тот, который вы продемонстрировали в своем вопросе.

person Valy    schedule 10.04.2017
comment
Большое спасибо, это, вероятно, объясняет то, что мы видим в другой, более сложной проблеме, поэтому я добавлю согласие к вашему ответу, поскольку он является наиболее подробным. - person Jan Heylen; 10.04.2017

Лучше остановись здесь

 printf("%lu,%d\n",a,b);

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

Цитирование C11, глава §7.21.6.1

[...] Если какой-либо аргумент не является правильным типом для соответствующей спецификации преобразования, поведение не определено.

person Sourav Ghosh    schedule 10.04.2017
comment
это действительно довольно «неопределенно», и вы, вероятно, правы, не следует пытаться объяснить это, но цель состоит в том, чтобы объяснить другую проблему, которая более сложна, чем этот небольшой пример. - person Jan Heylen; 10.04.2017

Важно понимать, что вы говорите.

printf("%lu,%d\n",a,b);

Интерпретируется printf() как:

  • Есть long unsigned
  • Есть int

В данном случае вы лжете - это неправда. Что делает это особенно плохим в вашем случае, так это то, что ваша система меняет размер unsigned long между 34- и 64-битным (как и у меня).

#include <stdio.h>

int main(void) {
        printf("sizeof(unsigned long): %zd\n", sizeof(unsigned long));
        printf("sizeof(unsigned long long): %zd\n", sizeof(unsigned long long));
        return 0;
}
$ gcc ll.c -o ll -m32 && ./ll
sizeof(unsigned long): 4
sizeof(unsigned long long): 8
$ gcc ll.c -o ll && ./ll
sizeof(unsigned long): 8
sizeof(unsigned long long): 8

Итак, да, это будет работать для 64-разрядных (по ошибке), но с 32-разрядными printf() берет значения неправильного размера из стека.

Очень важно убедиться, что строка формата соответствует аргументам.


Мы можем проверить это (игнорируя эти полезные предупреждения...):

#include <stdio.h>

int main(void) {
    unsigned long long x;
    int y;

    x = 0x8A7A6A5A4A3A2A1ALLU;
    y = 0x4B3B2B1B;

    printf("%lx - %x\n", x, y);
    printf("%llx - %x\n", x, y);

    return 0;
}
$ gcc ll.c -o ll -m32 && ./ll
ll.c: In function ‘main’:
ll.c:10:2: warning: format ‘%lx’ expects argument of type ‘long unsigned int’, but argument 2 has type ‘long long unsigned int’ [-Wformat=]
  printf("%lx - %x\n", x, y);
  ^
4a3a2a1a - 8a7a6a5a
8a7a6a5a4a3a2a1a - 4b3b2b1b
$ gcc ll.c -o ll && ./ll
ll.c: In function ‘main’:
ll.c:10:2: warning: format ‘%lx’ expects argument of type ‘long unsigned int’, but argument 2 has type ‘long long unsigned int’ [-Wformat=]
  printf("%lx - %x\n", x, y);
  ^
8a7a6a5a4a3a2a1a - 4b3b2b1b
8a7a6a5a4a3a2a1a - 4b3b2b1b

Вы можете видеть, что в 32-битном прогоне значение x разделяется на разрыв! printf() взял "первые" 32-битные, а затем "следующие" 32-битные, когда на самом деле мы предоставили ему одно 64-битное значение - порядок следования байтов делает это немного более запутанным.


Если вы хотите предписывать свои размеры переменных, взгляните на мой ответ здесь: https://stackoverflow.com/a/43186983/1347519

person Attie    schedule 10.04.2017