snprintf () печатает плавающие объекты мусора с помощью newlib nano

Я использую встроенную металлическую систему с ARM Cortex-M3 (STM32F205). Когда я пытаюсь использовать snprintf() с числами с плавающей запятой, например:

float f;

f = 1.23;
snprintf(s, 20, "%5.2f", f);

Я забиваю мусор в s. Формат, похоже, соблюдается, т.е. мусор представляет собой правильно сформированную строку с цифрами, десятичной точкой и двумя конечными цифрами. Однако, если я повторю snprintf, строка может измениться между двумя вызовами.

Математика с плавающей запятой, похоже, работает иначе, а snprintf работает с целыми числами, например:

snprintf(s, 20, "%10d", 1234567);

Я использую реализацию newlib-nano с переключателем компоновщика -u _printf_float. Компилятор arm-none-eabi-gcc.

У меня есть сильное подозрение на проблемы с распределением памяти, поскольку целые числа печатаются без каких-либо сбоев, а числа с плавающей запятой действуют так, как будто они были повреждены в процессе. Функции семейства printf вызывают malloc с плавающей запятой, а не с целыми числами.

Единственный фрагмент кода, не принадлежащий newlib, который я использую в этом контексте, - это мой _sbrk(), который требуется malloc.

caddr_t _sbrk(int incr)
{
  extern char _Heap_Begin; // Defined by the linker.
  extern char _Heap_Limit; // Defined by the linker.

  static char* current_heap_end;
  char* current_block_address;

  // first allocation
  if (current_heap_end == 0)
      current_heap_end = &_Heap_Begin;

  current_block_address = current_heap_end;

  // increment and align to 4-octet border
  incr = (incr + 3) & (~3);
  current_heap_end += incr;

  // Overflow?
  if (current_heap_end > &_Heap_Limit)
    {
    errno = ENOMEM;
    current_heap_end = current_block_address;
    return (caddr_t) - 1;
    }

  return (caddr_t)current_block_address;
}

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

Стек не должен сталкиваться с кучей, так как на них приходится около 60 КБ ОЗУ. Сценарий компоновщика может показаться безумным, но, по крайней мере, адреса кучи и стека кажутся правильными.


person DrV    schedule 26.02.2015    source источник
comment
Обратите внимание, что это doubles, а не floats. Понятия не имею, имеет ли это значение, вы все равно не можете передать float snprintf().   -  person unwind    schedule 26.02.2015
comment
Прототипом snprintf() является int snprintf(char *restrict s, size_t n, const char *restrict format, ...); Вы вызываете функцию не в соответствии с к прототипу. Вы #include <stdio.h> скомпилировали со всеми включенными предупреждениями?   -  person pmg    schedule 26.02.2015
comment
Уверены, что в вашем истинном коде используется snprintf(), а не sprintf()?   -  person chux - Reinstate Monica    schedule 26.02.2015
comment
@unwind: Хороший момент, но AFAIK printf - это функция с переменным числом аргументов, автоматически повышающая число с плавающей запятой до удвоения. (По крайней мере, gcc не жалуется на форматы с его педантичными настройками.) В моем исходном примере (буквальная константа 1.23) аргумент в любом случае является двойным, но в исправленном примере это число с плавающей точкой одинарной точности. Я не уверен насчет внутренностей newlib nano, я бы предположил, что он скорее сохраняет float как float, потому что двойники довольно трудоемки во встроенных системах (но это только предположение).   -  person DrV    schedule 26.02.2015
comment
@chux: Ага. Если у меня достаточно производительности, чтобы использовать какой-либо printf во встроенной системе, у меня достаточно производительности, чтобы считать символы, чтобы избежать каких-либо глупых происшествий. ИМХО, sprintf не должно существовать; это просто опасно.   -  person DrV    schedule 26.02.2015
comment
Просто у вас было snprintf(s, "%.2f", 1.23), и я подозревал, что вместо того, чтобы не получать / игнорировать предупреждения компилятора, произошла ошибка вырезания / вставки кода.   -  person chux - Reinstate Monica    schedule 26.02.2015
comment
@pmg: Спасибо, что заметили опечатку! Да, включены почти все предупреждения, а также -pedantic. Я был вынужден отказаться от -Werror, поскольку я использую STM32 HAL, который, к сожалению, вызывает ужасное количество предупреждений о заполнении и преобразовании.   -  person DrV    schedule 26.02.2015
comment
Я не слишком знаком с деталями реализации, но следующее вызывает некоторые легкие подозрения: %f обычно включает преобразование float в double; EABI требует 8-байтового выравнивания для double; ваш _sbrk() обеспечивает 4-байтовое выравнивание только того, что выдает. Насколько это важно, вероятно, зависит от чутья printf() и malloc(), но экспериментировать с этим должно быть несложно.   -  person Notlikethat    schedule 27.02.2015
comment
@Notlikethat: Звучит возможно. Я не упомянул об этом в своем вопросе, но на самом деле я пробовал выровнять кучу по 8 октетов без каких-либо изменений. Однако, поскольку аргументы передаются в стеке, это может быть проблемой выравнивания стека. Мне тоже придется проверить этот путь, спасибо!   -  person DrV    schedule 27.02.2015
comment
Можете ли вы сказать нам, сколько дополнительного места во флеш-памяти занято добавлением поддержки с плавающей запятой в printf с помощью -u _printf_float? Я пытаюсь скомпилировать часть размером 16 КБ (STM32F030F4P6), но двоичный файл кажется слишком большим (около 20 КБ).   -  person Christoph    schedule 18.06.2015


Ответы (2)


Поскольку может случиться так, что кто-то еще заразится той же ошибкой, я отправляю ответ на свой вопрос. Однако правильный ответ подсказал этот комментарий @Notliket.

Это урок Не укради. Я позаимствовал скрипт компоновщика gcc, который поставлялся с генератором кода STMCubeMX. К сожалению, скрипт вместе с файлом запуска не работает.

Соответствующая часть исходного скрипта компоновщика:

_estack = 0x2000ffff;

и его аналоги в сценарии запуска:

Reset_Handler:  
  ldr   sp, =_estack     /* set stack pointer */
...

g_pfnVectors:
  .word  _estack
  .word  Reset_Handler
...

Первая позиция вектора прерывания (0) всегда должна указывать на вершину стека запуска. Когда достигается прерывание сброса, оно также загружает указатель стека. (Насколько я могу сказать, последний не нужен, поскольку HW в любом случае перезагружает SP из 0-го вектора перед вызовом обработчика сброса.)

Указатель стека Cortex-M всегда должен указывать на последний элемент в стеке. При запуске в стеке нет элементов, поэтому указатель должен указывать на первый адрес над фактической памятью, в данном случае 0x020010000. В исходном скрипте компоновщика указатель стека устанавливается на 0x0200ffff, что фактически приводит к sp = 0x0200fffc (аппаратное обеспечение заставляет стек с выравниванием по словам). После этого стек смещается на 4.

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

MEMORY
{
FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 128K
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 64K
}

_stacktop = ORIGIN(RAM) + LENGTH(RAM);

После этого значение _stacktop равно 0x20010000, и мои числа красиво плавают ... Та же проблема может возникнуть с любой внешней (библиотечной) функцией, использующей параметры двойной длины, поскольку ARM Cortex ABI утверждает, что стек должен быть выровнен по 8 октетам, когда вызов внешних функций.

person DrV    schedule 27.02.2015
comment
Та же проблема с образцом STM32 на CubeMX для Nucleo F401, вы сэкономите мне много времени. Спасибо - person rom1nux; 10.06.2019
comment
У меня такая же проблема с моим psoc, который также использует newlib-nano. Есть ли шанс, что вы можете подробнее рассказать, как вы это исправили. Раньше я не работал с настройкой скриптов компоновщика - person Typhaon; 06.11.2020

snprintf принимает размер в качестве второго аргумента. Вы можете просмотреть этот пример http://www.cplusplus.com/reference/cstdio/snprintf/

/* snprintf example */
#include <stdio.h>

int main ()
{
  char buffer [100];
  int cx;

  cx = snprintf ( buffer, 100, "The half of %d is %d", 60, 60/2 );

  snprintf ( buffer+cx, 100-cx, ", and the half of that is %d.", 60/2/2 );

  puts (buffer);

  return 0;
}
person theadnangondal    schedule 26.02.2015
comment
Хороший ответ, и да, я действительно мог совершить эту ошибку ... Но в этом случае gcc (еще раз) сказал бы мне, что я глуп. Итак, на этот раз проблема, похоже, лежит где-то глубже в newlib - или, возможно, в сценариях компоновщика. - person DrV; 26.02.2015