Заминированный retarget printf в CooCox/CoIDE

Стал разбираться в последнем CoIDE с semihosting (возможность делать вывод в консоль через JTAG/SWD) и обнаружил интересный баг, на разбор которого убил некоторое время. Поскольку мина уже обезврежена, то делюсь с теми, кто ещё не успел на ней подорваться.

Подорвался я на том, что вывод SH_SendString(«abc») успешно попадал в консоль, а вывод printf(«abc\n») туда никак не попадал. При этом надо сказать, что semihosting используется в retarget printf, который находится в файле stdio/printf.c внутри проекта. Там стоит пустая реализация PrintChar, которая заменяется на вот такую:

void PrintChar(char c)
{
	SH_SendChar((int)c);
}

Отладка показала, что GCC выполняет builtin printf optimization и в случае этой конкретной строки заменяет вызов printf(«abc\n») на puts(«abc»); Далее, через цепочку вложеных вызовов всё это попадает в PrintChar (того же самого stdio/printf.c), который вызывает уже SH_SendChar для передачи символа через отладочный адаптер.

Реализован SH_SendChar следующим образом:

static char g_buf[16];
static char g_buf_len = 0;

void SH_SendChar(int ch) {
	g_buf[g_buf_len++] = ch;
	g_buf[g_buf_len] = '\0';
	if (g_buf_len + 1 >= sizeof(g_buf) || ch == '\n' || ch == '\0') {
		g_buf_len = 0;
		/* Send the char */
		if (SH_DoCommand(0x04, (int) g_buf, NULL) != 0) {
			return;
		}
	}
}

Здесь очень хорошо видно, что функция заполняет буфер полностью, либо до получения символов '\n' или '\0'. После чего отсылает буфер в консоль через отладочный адаптер.

Но мина была заложена как раз в том месте, где бойцы из команды CooCox забыли добавить к строке символ '\n' внутри функции puts. Таким образом SH_SendChar никогда не получала символы '\n' или '\0' и ничего не передавала в консоль до заполнения буфера целиком. И если выводилась строка длинной меньше размера буфера — в консоль ничего не попадало, пока буфер не заполнялся последующим выводом.

Правится это тривиально, нужно добавить всего однин вызов fputc('\n', pStream) перед возвратом из функции fputs в файле stdio/printf.c
UPDATE: На самом деле, это нужно править на уровне функции puts в файле stdio/printf.c (см. комментраии к статье), следующим образом:

signed int puts(const char *pStr)
{
    signed int i = fputs(pStr, stdout);
    fputc('\n', stdout);

    return i+1;
}

p.s. про builtin optimisations хорошо написано здесь, тут и вот тут.
  • +3
  • 20 мая 2012, 02:10
  • dipspb

Комментарии (19)

RSS свернуть / развернуть
странно, почему он корёжит литерал при оптимизации.
0
Посмотрите по ссылкам — там хоть ина английском, но хорошо описано как и почему это происходит. Признаться, поскольку я давно уже плотно не работал на сях, для меня это тоже было откровением. Видимо крепко забыл =)

Это одна из стандартных оптимизаций GCC. Более того — так делает не только GCC, по одной из ссылок это тоже указывается.

Литерал он меняет потому, что при подмене printf на puts, '\n' в конце строки ненужен. А ненужен он потому, что puts всегда неявно добавляет его к концу строки самостоятельно. То есть, puts — это всегда с переводом строки.

Если вы напишете printf(«abc»), без '\n' на конце, то GCC не станет делать такую оптимизацию. Поскольку puts тут уже не подходит. И если напишете int i = printf(«abc\n»), тоже не станет. Хотя здесь и есть явный перевод строки в литерале, но требуется возвращать значение. А puts это не делает.
0
ЗАбыл добавить… Такие оптимизации отключаются флагами компилятора. Можно запретить вообще все оптимизации сразу с помощью -O0. А можно запретить конкретные отдельные оптимизации. По ссылкам всё хорошо описано.

Оптимизация printf даёт выигрыш в производительности вывода примерно в два раза.
0
Как-то оно не совсем правильно, чтоб fputs перевод строки всегда слала. Есть ведь fflush, он должен сбрасывать буфер.
0
Смотрите… Вы одновременно и правы и нет =)

Собирался аргументированно ответить, дескать функции puts/fputs, в соответствии со стандартом на stdio, должны сами добавлять '\n' в конце строки. И вдруг обнаружил, что это касается только puts! Функция fputs, в отличии от первой, этого не делает.

Поправлю решение в статье — фикс должен быть уровнем выше, внутри puts.

Что касается flush/fflush, тут всё иначе. Мы говорим не о чистом stdio, мы говорим об огрызке полноценного printf, который «оптимизирован» (в основном упрощением), называется retarget printf и предназначен (в т.ч.) для вывода через semihosting. В этом решении нет ничего такого, подобного flush/fflush, что могло бы «подтолкнуть» вывод через semihosting. Догадываюсь, что добавить это не сложно, но «из коробки» там этого нет.

Да и буферизаци semihosting сделана вполне разумно. Не уверен, что есть смысл добавлять flush.
0
А у меня в окно семихостинга вообще перестало выводиться. В версии CoIDE 1.3.1 всё работало. В версиях 1.4.0 и 1.4.1 — нет.
0
как Вы туда выводите? покажите пожалуйста
0
Сейчас пока нет возможности показать. Попозже сделаю. Может завтра или послезавтра. В 1.3.1 всё чётко работало.
0
Ну Вы киньте сюда когда будет время, а я потом тоже гляну как смогу. На форуме у них были жалобы на подобное, но какие-то немногочисленные и не слишком внятные.
0
Там у них и ещё каки-бяки были. Об одной я уже писАл у них на форуме: www.coocox.org/forum/topic.php?id=911 (извиняюсь за плохой английский).
0
Собственно, есть один пример от angel5a, адаптированный к CoIDE, где данные от LM75 выводятся в окно семихостинга. В CoIDE-1.3.1 всё работало. В 1.4.0 и 1.4.1 — не фига.
Вот попробовал наипростейшее, что только можно придумать: pluslab.ru/dif/SemiHosting_03.rar
Всё тоже самое: В CoIDE-1.3.1 всё работает.
В 1.4.0 и 1.4.1 — нет. Останавливается на строке 32 файла sh_cmd.s:
BKPT 0xAB                 		/* Wait ICE or HardFault */

Пробовал не только тупо перенести проект из 1.3.1 в 1.4.1, но и полностью заново его набрать: мало ли какая кака-бяка могла туда затесаться? Результат тот же.
0
Если не ошибаюсь, то это простая засада… Не скажу как в более старых версиях, но в свежей надо в конфигурации запуска поставить галку в чекбоксе «Semihosting Enable».

Без этого будет именно в этом месте бесконечно останавливаться. Тоженарывался на это.
0
Галочку ставил. Просто в новых версиях так. Причём пробовал на трёх разных компах. На одном из которых «семёрка» стояла, а на двух — ХРюша. Результат один.
0
Ну вот, новости от кокоса:
CooCox CoIDE V1.4.2 5/24/2012

[CooCox CoIDE-Improved]
Added: Support download/debug for Nuvoton Nano100 series
Corrected: the bug that ARM Embeded GCC (2012Q1 V1) do not support J-Link debug
Corrected: the bug that J-Link adapter configuration cannot cancel run to main
Corrected: the bug that semihosting cannot work
Corrected: the bug that Erase Full Chip don't working
Попробую сегодня вечером. Отпишусь.
0
Попробовал в 1.4.2. Да, семихостинг работает. Правда не так, как в старой доброй 1.3.1. Там в окне семихостинга в результате работы «навороченной» проги:
int main(void)
{
	printf ("The Beginning...\n\r");
	while(1)
    {
		delay ();
		printf ("Next Step\n\r");
    }
}

выводилось следующее:
The Beginning…
Next Step
Next Step
.........
Сейчас же выводится так:
The Beginning..The Beginning..The Beginning..The Beginning..The Beginning..
То есть циклится то, что должно выводиться один раз. Да и то, в этой строке теряется одна последняя точка.
0
У меня зработало так:
coocox.org — Example for Semihosting
Версия CoIDE — 1.4.2
0
Сорри, погорячился.
Работает, но как-то странно, и не стабильно (в т.ч. вариант из статьи).
То потухнет, то погаснет, то вообще не светит ):
0
В недавно вышедшей версии CoIDE 1.5.1 retarget print заработал, как было в старой доброй 1.3.1.
0
  • avatar
  • Plus
  • 02 ноября 2012, 00:12
Хто поможет днищу освоить азы программирования? Мой контакт Вконтакте vk.com/dmitrii_bui, e-mail dmitrii_bui@mail.ru. Заранее спасибо если хто то отзовется.
-2
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.