Variadic функции без `...` - PullRequest
       91

Variadic функции без `...`

0 голосов
/ 08 ноября 2018

В x86_64 / Linux, скомпилирован с помощью gcc / clang -O3:

void void_unspec0(),void_unspec1(),void_unspec2(),void_unspec3(),void_void(void);

void call_void_void()
{
    void_void();
    void_void();
    void_void();
    void_void();
    void_void();
}

void call_void_unspec()
{
    void_unspec0();
    void_unspec0();
    void_unspec0();
    void_unspec0();
    void_unspec1(.0,.0,.0);
    void_unspec2(.0,.0,.0,.0,.0,.0,.0,.0);
    void_unspec3(.0,.0,.0,.0,.0,.0,.0,.0,.0,.0);
}

разбирает на:

0000000000000000 <call_void_void>:
   0:   48 83 ec 08             sub    $0x8,%rsp
   4:   e8 00 00 00 00          callq  9 <call_void_void+0x9>
   9:   e8 00 00 00 00          callq  e <call_void_void+0xe>
   e:   e8 00 00 00 00          callq  13 <call_void_void+0x13>
  13:   e8 00 00 00 00          callq  18 <call_void_void+0x18>
  18:   48 83 c4 08             add    $0x8,%rsp
  1c:   e9 00 00 00 00          jmpq   21 <call_void_void+0x21>
  21:   66 66 2e 0f 1f 84 00    data16 nopw %cs:0x0(%rax,%rax,1)
  28:   00 00 00 00 
  2c:   0f 1f 40 00             nopl   0x0(%rax)

0000000000000030 <call_void_unspec>:
  30:   48 83 ec 08             sub    $0x8,%rsp
  34:   31 c0                   xor    %eax,%eax
  36:   e8 00 00 00 00          callq  3b <call_void_unspec+0xb>
  3b:   31 c0                   xor    %eax,%eax
  3d:   e8 00 00 00 00          callq  42 <call_void_unspec+0x12>
  42:   31 c0                   xor    %eax,%eax
  44:   e8 00 00 00 00          callq  49 <call_void_unspec+0x19>
  49:   31 c0                   xor    %eax,%eax
  4b:   e8 00 00 00 00          callq  50 <call_void_unspec+0x20>
  50:   66 0f ef d2             pxor   %xmm2,%xmm2
  54:   b8 03 00 00 00          mov    $0x3,%eax
  59:   66 0f ef c9             pxor   %xmm1,%xmm1
  5d:   66 0f ef c0             pxor   %xmm0,%xmm0
  61:   e8 00 00 00 00          callq  66 <call_void_unspec+0x36>
  66:   66 0f ef ff             pxor   %xmm7,%xmm7
  6a:   b8 08 00 00 00          mov    $0x8,%eax
  6f:   66 0f ef f6             pxor   %xmm6,%xmm6
  73:   66 0f ef ed             pxor   %xmm5,%xmm5
  77:   66 0f ef e4             pxor   %xmm4,%xmm4
  7b:   66 0f ef db             pxor   %xmm3,%xmm3
  7f:   66 0f ef d2             pxor   %xmm2,%xmm2
  83:   66 0f ef c9             pxor   %xmm1,%xmm1
  87:   66 0f ef c0             pxor   %xmm0,%xmm0
  8b:   e8 00 00 00 00          callq  90 <call_void_unspec+0x60>
  90:   66 0f ef c0             pxor   %xmm0,%xmm0
  94:   6a 00                   pushq  $0x0
  96:   66 0f ef ff             pxor   %xmm7,%xmm7
  9a:   6a 00                   pushq  $0x0
  9c:   66 0f ef f6             pxor   %xmm6,%xmm6
  a0:   b8 08 00 00 00          mov    $0x8,%eax
  a5:   66 0f ef ed             pxor   %xmm5,%xmm5
  a9:   66 0f ef e4             pxor   %xmm4,%xmm4
  ad:   66 0f ef db             pxor   %xmm3,%xmm3
  b1:   66 0f ef d2             pxor   %xmm2,%xmm2
  b5:   66 0f ef c9             pxor   %xmm1,%xmm1
  b9:   e8 00 00 00 00          callq  be <call_void_unspec+0x8e>
  be:   48 83 c4 18             add    $0x18,%rsp
  c2:   c3                      retq   

Во втором случае (call_void_unspec()) компилятор считает аргументы с плавающей запятой, переданные в регистрах, предположительно потому, что спецификация SysVABI / AMD64 говорит, что должен.

Когда вызывается функция, принимающая переменные-аргументы, %rax должен быть установлен в общее количество параметров с плавающей запятой, переданных функции в Регистры SSE

В чем причина правила в спецификации ABI? Должны ли непрототипированные вызовы функций соблюдать его, учитывая, что функции, определенные с помощью ... (многоточие), должны быть прототипированы (6.5.2.2p6) перед вызовом? Могут ли функции без ... быть также вариативными?

Ответы [ 2 ]

0 голосов
/ 08 ноября 2018

Могут ли функции без ... тоже быть вариативными?

Пункт 6.5.2.2/6 стандарта, пожалуй, наиболее актуален:

Если выражение, обозначающее вызываемую функцию, имеет тип, который не включает в себя прототип , целочисленные продвижения выполняются на каждый аргумент и аргументы с типом float повышаются до двойной. Это называется продвижением аргументов по умолчанию. Если количество аргументов не равно количеству параметров, поведение не определено.

(выделение добавлено.) Это тот случай, когда объявленный тип функции не содержит списка параметров (в отличие от списка параметров, состоящего только из void). Вызывающий по-прежнему отвечает за передачу правильного количества параметров.

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

Это отличает свойства функции definition от типа подвыражения функции call , которая обозначает функцию. Обратите внимание, что в нем явно говорится, что поведение вызова функции с переменным значением через выражение функции, тип которой не включает прототип, не определено. Также требуется сопоставление типов между продвигаемыми аргументами и параметрами.

Если функция определена с типом, который не включать прототип и типы аргументов после продвижения несовместимы с параметрами параметров после поведение не определено, за исключением следующих случаев:

  • один повышенный тип является целочисленным типом со знаком, другой повышенный тип является целочисленным типом без знака, и значение равно представимый в обоих типах;
  • оба типа являются указателями на квалифицированные или неквалифицированные версии типа символа или void.

Это случай определения функции в стиле K & R. Это также требует совпадения чисел и типов между аргументами и параметрами, поэтому такие функции не являются переменными.

Таким образом,

В чем причина правила в спецификации ABI? Должен быть непрототипирован вызовы функций соблюдаются при условии, что функции определены с ... (многоточие) требуется прототип?

Я полагаю, что причина этого правила - указывать, какие регистры FP должны быть сохранены или сохранены реализацией функции. Поскольку вызов функции с переменным числом посредством выражения функции, тип которого не включает прототип, имеет UB, реализация C не имеет особой необходимости следовать этому положению ABI.

0 голосов
/ 08 ноября 2018

Что говорит стандарт C

Обратите внимание, что функции variadic могут вызываться только при наличии прототипа. Если вы попытаетесь вызвать printf() без прототипа, вы получите UB (неопределенное поведение).

C11 §6.5.2.2 Вызов функции ¶6 говорит:

If6 Если выражение, обозначающее вызываемую функцию, имеет тип, который не содержит прототип, целочисленные преобразования выполняются для каждого аргумента, а аргументы, имеющие тип float, переводятся в double. Они называются продвижениями аргументов по умолчанию. Если число аргументов не равно количеству параметров, поведение не определено. Если функция определена с типом, который включает в себя прототип, и либо прототип заканчивается многоточием (, ...), либо типы аргументов после продвижения не совместимы с типами параметров, поведение не определено. Если функция определена с типом, который не содержит прототип, и типы аргументов после продвижения не совместимы с типами параметров после продвижения, поведение не определено, за исключением следующих случаев:

  • один повышенный тип является целочисленным типом со знаком, другой повышенный тип является целочисленным типом без знака, и значение может быть представлено в обоих типах;
  • оба типа являются указателями на квалифицированные или неквалифицированные версии типа символа или void.

Применительно к исходному коду в вопросе

Исходный код в вопросе был похож на это - последовательные идентичные вызовы функций были уменьшены до одного вызова.

void void_unspec(), void_void(void);

void call_void_void()
{
    void_void();
}

void call_void_unspec()
{
    void_unspec();
    void_unspec(.0,.0,.0);
    void_unspec(.0,.0,.0,.0,.0,.0,.0,.0);
    void_unspec(.0,.0,.0,.0,.0,.0,.0,.0,.0,.0);
}

Этот код вызывает UB, потому что количество аргументов для вызовов функции к void_unspec() не все совпадают с числом аргументов, которые он определен принять (независимо от того, что это определение; он не может одновременно принимать 0, 3, 8 и 10 аргументов). Это не нарушение ограничений, поэтому диагностика не требуется. Компилятор обычно делает все, что считает наилучшим для обратной совместимости, и обычно не вызывает явных сбоев, но любые проблемы, которые возникают у программиста за нарушение правил стандарта.

И поскольку стандарт говорит, что поведение не определено, нет конкретной причины, по которой компилятор должен устанавливать %rax (конечно, стандарт C ничего не знает о %rax), но простая согласованность предполагает, что он должен .

Применительно к пересмотренному коду в вопросе

Код в вопросе был изменен следующим образом (повторные последовательные вызовы снова пропущены):

void void_unspec0(), void_unspec1(), void_unspec2(), void_unspec3(), void_void(void);

void call_void_void()
{
    void_void();
}

void call_void_unspec()
{
    void_unspec0();
    void_unspec1(.0,.0,.0);
    void_unspec2(.0,.0,.0,.0,.0,.0,.0,.0);
    void_unspec3(.0,.0,.0,.0,.0,.0,.0,.0,.0,.0);
}

Код больше не неизбежно вызывает неопределенное поведение. Однако там, где определены функции void_unspec0() и т.д., они должны выглядеть примерно так:

void void_unspec0(void) { … }
void void_unspec1(double a, double b, double c) { … }
void void_unspec2(double a, double b, double c, double d, double e, double f, double g, double h) { … }
void void_unspec3(double a, double b, double c, double d, double e, double f, double g, double h, double i, double j) { … }

Одна эквивалентная запись будет:

void void_unspec2(a, b, c, d, e, f, g, h)
    double a, b, c, d, e, f, g, h;
{
    …
}

Используется нестандартное определение K & R, не являющееся прототипом.

Если определения функций не соответствуют этим, то в §6.5.2.2¶6 говорится, что результатом вызовов является неопределенное поведение. Это избавляет стандарт от необходимости законодательно определять, что происходит при всевозможных сомнительных обстоятельствах. Как и прежде, компилятор может передавать число значений с плавающей запятой в %rax; в этом есть смысл. Но очень мало что можно сделать, чтобы спорить о том, что произойдет - либо вызовы соответствуют определению, и все в порядке, либо нет, и есть неуказанные (и неуказанные) потенциальные проблемы.

Обратите внимание, что ни call_void_void(), ни call_void_unspec() не определены с прототипом. Обе эти функции принимают нулевые аргументы, но нет видимого прототипа, который обеспечивал бы это, поэтому код в том же файле мог вызвать call_void_void(1, "abc") без жалоб компилятора. (В этом отношении, как и во многих других, C ++ - это другой язык с другими правилами.)

...