Роль LibC в Linux. Зауважте, що glibc надає значно більше інструментів, ніж стандартна бібліотека С -- зокрема, засоби POSIX. Нижче мови про них не буде. (c) wiki |
Увага (як часто буває в embedded!), багато що із написаного нижче дуже сильно прив'язано до особливостей конкретного компілятора, GCC та конкретних його версій! Про Keil, можливо, колись напишу окремо. З іншого боку, особливої прив'язки до IDE немає, CoIDE згадується лише як конкретний приклад, котру галочку в котрому меню слід поставити для досягнення якогось результату. Розгляд ведеться для ARM-GCC 4.8 та 4.9, для новіших чи старіших версій можуть бути відмінності.
Стандартна бібліотека C
Стандартна бібліотека С включає купу корисних речей, до яких ми звикли. Однак, її реалізація вимагає підтримки з боку операційної системи, зокрема, наявності системних викликів, якими бібліотека може скористатися для виконання своєї роботи. Наприклад, функція printf() сама не працюватиме із консоллю --- в кінці кінців, вона, на більшості платформ, просто не матиме до неї фізичного доступу. Тому, для виводу звертатиметься до операційної системи. Аналогічно, malloc() -- йому потрібно якось запитувати пам'ять у менеджера пам'яті. [Так, навіть на мікроконтролерах динамічна пам'ять іноді цілком доречна! Хоча, з нею, звичайно, потрібно поводитися акуратно.]
Однак, наша програма на контролері працює на голому залізі (bare metal) --- там просто немає операційної системи. [Про RTOS поговоримо окремо.]
Для вирішення цієї проблеми багато бібліотек для контролерів, (не тільки реалізації стандартної бібліотеки C), пишуть, використовуючи невелику кількість "системних функцій", які користувач-програміст повинен реалізувати для тієї платформи, на якій працює.
Щодо стандартної бібліотеки С, CoIDE підтримує обидва варіанти стандартної бібліотеки, які ідуть разом із компілятором ARM GCC: Newlib від Red Hat (на вкладці Configure/Link оболонка називає її "base C library"), та Newlib-nano, (яку вона називає "nano С library", див. також офіційну сторінку), мініатюризовану модифікацію Newlib, розроблену людьми, що підтримують ARM GCC.
Newlib -- реалізація стандартної бібліотеки С, розроблена для того, що зараз називається Cygwin -- POSIX-шар для Win32, поверх якого можна встановити практично будь-яку програму із світу Linux (а то й UNIX взагалі). Потім, завдяки "лаконічності" необхідних викликів, перепрофілювалася в реалізацію стандартної бібліотеки С для вбудованих систем.
Додатково можна глянути і на інші реалізації, скажімо, uClibc. Порівняння різних реалізацій можна побачити, наприклад, на сторінці musl -- ще однієї реалізації. Більшість перерахованих там нам не підійдуть --- вони вимагають наявності в процесорі MMU (Memory management unit -- грубо кажучи, засіб організації віртуальної пам'яті), якого на контролерах класу ARM Cortex M немає (не плутати із MPU). Однак, це порівняння добре ілюструє, скільки всілякої роботи робить стандартна бібліотека С, і наскільки по різному вона це може робити.
Компонента CoIDE, C_library (див. попередній пост) надає мінімальну реалізацію частини із цих функцій. Знаходиться ця реалізація у файлі syscalls.c (Нагадаю, що єдиний спосіб їх редагувати, який мені вдалося знайти в CoIDE 2 --- перетягнути файл у віртуальну папку проекту). Опис їх є, наприклад, в документації newlib: "12.1 Definitions for OS interface".
Розглянемо їх, розширивши і доповнивши мінімалістичну реалізацію від CoIDE. (В результаті, я до своїх проектів додаю описану тут реалізацію, навіть не включаючи згадану компоненту. Для нетерплячих --- демонстраційний проект, із всім кодом, тут.)
Популярний, хоч і не стандартизований системний виклик -- збільшити розмір купи (heap), з якої виділяється динамічна пам'ять. Необхідний для роботи malloc() і всього, що від нього залежить. Повинен повертати попереднє значення вершини купи. (Альтернатива на "великих" системах -- mmap, зараз її не обговорюватимемо.)
У нашому випадку, щоб лінкер цю функцію знайшов, слід спереду додати символ підкреслення. Реалізація, яка йде із CoIDE, наступна:
Вона покладається на символ _end, створений лінкером. Увага -- це не змінна! Його адреса вказує на початок пам'яті, де можна створити купу, або, якщо простіше -- на її початок. Нестандартний атрибут __attribute__ ((used)) не дає оптимізатору "грохнути" її випадково, навіть якщо видається, що функція не використовується.
Робота її менш-більш очевидна. Однак, стек рухається назустріч купі, і вони можуть затерти один одного. Частково запобігти цьому відносно просто (взято із змінами з посилань нижче):
У нашому випадку, щоб лінкер цю функцію знайшов, слід спереду додати символ підкреслення. Реалізація, яка йде із CoIDE, наступна:
extern int _end;
__attribute__ ((used)) caddr_t _sbrk ( int incr ) { static unsigned char *heap = NULL; unsigned char *prev_heap; if (heap == NULL) { heap = (unsigned char *)&_end; } prev_heap = heap; heap += incr; return (caddr_t) prev_heap; }
Вона покладається на символ _end, створений лінкером. Увага -- це не змінна! Його адреса вказує на початок пам'яті, де можна створити купу, або, якщо простіше -- на її початок. Нестандартний атрибут __attribute__ ((used)) не дає оптимізатору "грохнути" її випадково, навіть якщо видається, що функція не використовується.
Особливо гостро потреба в цьому атрибуті постає за використання LTO. Характерна ознака, що ви забули про нього у якомусь важливому місці -- помилки компонування виду "`_write' referenced in section `.text._write_r' of c:/embedded/gcc-arm-none-eabi-4_9-2015q3-20150921-win32/bin/../lib/gcc/arm-none-eabi/4.9.3/../../../../arm-none-eabi/lib/armv7-m\libg.a(lib_a-writer.o): defined in discarded section `.text' of ..\obj\syscalls.o (symbol from plugin)". Див. також тут: "Bug 54933 - 'builtin symbol' referenced in section ... defined in discarded section".
Робота її менш-більш очевидна. Однак, стек рухається назустріч купі, і вони можуть затерти один одного. Частково запобігти цьому відносно просто (взято із змінами з посилань нижче):
extern int _end; /*This function is used for handle heap option*/ __attribute__ ((used)) caddr_t _sbrk ( int incr ) { static unsigned char *heap = NULL; unsigned char *prev_heap; extern unsigned char *__StackLimit; /* Value set by linker */ if (heap == NULL) { heap = (unsigned char *)&_end; } prev_heap = heap; if (heap + incr > &__StackLimit) { #ifdef PRINT_MESSAGE_ABOUT_HEAP_AND_STACK_COLLISION char err_msg[] = "Heap and stack collision\n"; _write (1, err_msg, strlen(err_msg)); // Потребує <string.h> #endif errno=ENOMEM; // Потребує <errno.h> return (void *) -1; } heap += incr; return (caddr_t) prev_heap; }
Частково -- бо заборонити стеку рости трохи важче... Реалізація із додатковими можливостями відладки буде розглядатися в одному із наступних постів.
__StackLimit -- ще один символ, оголошений лінкером. (Назва може залежати від конфігурації компілятора!). За бажання (вираженням якого керуємо із допомогою макросу PRINT_MESSAGE_ABOUT_HEAP_AND_STACK_COLLISION), можна виводити повідомлення про помилку (наприклад, засобами semihosting -- див. далі). Використовуємо для виведення _write(), про яку теж далі -- printf() використовувати не можна! Навіть не говорячи про її громіздкість, вона може покладатися на malloc(). (З іншого боку, використано strlen(), функція ця відносно дешева, а вручну підраховувати кількість символів у стрічці помилок, як в офіційних прикладах --- трохи дурна річ).
Важливе зауваження.
Через те, що повертається попередня вершина купи, функція не-реентрантна (не повертається мені язик її неповновикористовною називати...), тому її (та функції, що можуть виділяти пам'ять!) не рекомендується використовувати у обробниках переривань! Реентрантні варіанти функцій стандартної бібліотеки тут теж не дуже допоможуть --- купа глобальна. Тому, якщо дуже треба (а добре подумайте, чи справді дуже!), слід перевіряти мютекс на вході, але не блокувати, якщо "зайнято", а просто повертати помилку -- інакше буде deadlock.
Детальніше див.:
- "12.1 Definitions for OS interface" та "12.2 Reentrant covers for OS subroutines" в офіційній документації Newlib
- "5.3.15. Allocate more Heap, sbrk" та "5.4. Reentrant System Call Implementations" із "Howto: Porting newlib. A Simple Guide" від Jeremy Bennett, Embecosm.
- Загальна стаття про алокатори (переклад): "Управление памятью: Взгляд изнутри"
Однак, для printf() чи й навіть puts(), самого sbrk() буде мало. Щоб переконатися, достатньо закоментувати реалізацію всіх ф-цій із syscalls.c, крім sbrk(), в програму вставити malloc() і подивитися на помилки компонування. Потрібно ще: _write(), _close(), _fstat(), _isatty(), _lseek(), _read(). Розглянемо їх, найважливішу для нас, _write(), відклавши на десерт.
_close()
Призначення -- закрити файл. Якщо файли (навіть спеціальні, в стилі UNIX) не підтримуються, а UART чи semihosting служать в ролі термінала/консолі (стандартних потоків вводу та виводу), можна скористатися найпростішою реалізацією, яка ніколи не завершується успішно.
__attribute__ ((used)) int _close(int file) { errno = EBADF; return -1; }
Можна також реалізувати механізм своїх спеціальних файлів --- скажімо, 0 це UART1, 1 -- UART2, 3 -- SemiHosting, тощо.
_fstat()
Функція ця повертає статус файлу. Детальніше про цю функцію див., наприклад, "fstat(2) - Linux man page". Якщо підтримку файлів не реалізовано, розумним буде будь-який наш файл вважати символьним пристроєм (див. також опис close()), повідомивши про це за допомогою значення S_IFCHR поля st_mode структури stat, із описом статусу файлу (подробиці див. за посиланням вище):
__attribute__ ((used)) int _fstat(int file, struct stat *st) { st->st_mode = S_IFCHR; return 0; }
_isatty()
Перевіряє, чи потік працює із терміналом. Вважаємо (поки), що завжди -- так.
Якщо б (ті чи інші) файли (як мінімум -- файлові дескриптори) таки підтримувалися, вона могла б виглядати якось так:
Це ж стосується інших пов'язаних функцій.
_lseek()
Переміщення файлового вказівника, повертає його нове положення відносно початку файлу. Див., наприклад, "lseek - move the read/write file offset". Будемо вважати, що по нашому потоку соватися не можна:
Точніше, можна -- це не є помилковою ситуацією (в якій ф-ція мала б повернути -1 і встановити errno), просто далі початку не рухаємося.
_read()
Читає вказану кількість байтів із файлу, повертає, скільки реально прочитано. Детальніше див., наприклад: "read(3) - Linux man page". Вважаємо, що не маємо звідки читати:
Якщо хотілося б додати можливість читати, скажімо, із UART, можна написати щось таке:
_write() і SemiHosting
Записує вказану кількість символів у файл, повертає, скільки реально записано. Детальніше, див., наприклад, "write(3) - Linux man page". Звичайно, в дусі мінімалізму, що господарює вище, можна б було просто повертати 0. Однак, нам би хотілося мати можливість якогось більш звичного "feedback" від демоплати, ніж різноманітні мигання світлодіодами (занудно вважаючи, що постійно запалений світлодіод -- це мигання із безмежним періодом :-).
Контролери сімейства STM32 володіють зручним вбудованим засобом для цього --- SemiHosting. Зокрема, цей засіб дозволяє пересилати літери відладчику. Детальніше про нього можна почитати тут:
Де SH_SendChar() --- функція, оголошена в semihosting.h, котра передає один символ дебаггеру. (Звичайно, замість неї може бути яка-небудь UART_PutChar(), для передачі з використанням UART). Її реалізація знаходиться в semihosting.c, думаю, вартує на неї глянути:
Зауважте внутрішню буферизацію --- поки буфер не закінчився, або не передано нульовий символ чи символ кінця рядка, нічого не пересилатиметься.
Ось, власне і все --- після реалізації перерахованих вище функцій, printf() працюватиме:
errno
Newlib всередині, замість змінної, використовує макрос errno, для спрощення роботи реентрантних функцій. Однак, для функцій, що реалізовують системні виклики, цей підхід не працюватиме. Тому доводиться в syscalls.c викручуватися так:
Див. також "Error Handling" із "Howto: Porting newlib. A Simple Guide" від Jeremy Bennett, Embecosm та відповідну частину офіційної документації. Взагалі, ці два джерела стосуються практично всіх розглянутих функцій та змінних.
environ
Вказівник на масив вказівників на стрічки із змінними середовища, який закінчується порожньою С-стрічкою. В найпростішому варіанті -- порожній, тобто містить лише згадану порожню стрічку (вказівник на неї):
_exit()
Негайно завершити програму, не здійснюючи жодних спроб прибрати за собою. Контролер в такій ситуації може хіба зависнути, увійшовши в нескінчений цикл.
_execve()
Виконати програму (зазвичай -- замістивши нею процес, створений за допомогою fork()). Детальніше, про те, що це за виклик і як має поводитися, див. "execve - execute program". Підтримки запуску процесів у нас немає:
_fork()
Створити новий процес, на початку -- копію поточного. Детальніше див. "fork(2) - Linux man page". Підтримки "розмноження" процесів у нас немає:
_getpid()
Отримати PID (Process ID) поточного процесу. Можна вважати, що процес у нас один:
_kill()
Послати сигнал процесу. (Насправді, не зважаючи на назву, вона далеко не завжди вбиває отримувача, подробиці див., наприклад, тут: "kill(2) - Linux man page"). За відсутності потенціальних отримувачів, реалізація проста:
_link()
Перейменувати файл. За відсутності підтримки файлів:
_open()
Відкрити файл. За відсутності підтримки файлів:
_times()
Повертає різноманітні часи процесу -- скільки і як виконувався (скільки власне сам процес, скільки часу провів у системних викликах, скільки часу у дочірніх процесах, детальніше див. тут). Певна осмислена реалізація цілком може існувати навіть у нашому, мінімалістичному випадку, але поки обмежимося відмовою повідомляти цю інформацію:
_unlink
Видаляє файл. За відсутності підтримки файлів:
_wait
Очікувати на дочірній процес. За відсутністю таких:
Ось і все.
УВАГА! Вище, в педагогічних цілях, наведено повний комплект функцій, що реалізовують системні виклики, необхідні Newlib. Однак, включення тих із них, що непотрібні, особливо із атрибутом used, може привести до марної витрати пам'яті програм (flash-пам'яті) мікроконтролера (може не приводити, особливо з LTO). Тому, в проекті варто залишати лише необхідні.
Аналогічно, хоча послідовна робота із errno важлива, якщо ним не користуються, відповідні рядки коду можна прибрати, зекономивши десятки байт прошивки. З іншого боку, передбачуваність поведінки стандартної бібліотеки, відповідність її очікуванням, теж важлива.
Як би там не було, готовий проект, що демонструє описане в цьому пості, тобто реалізацію цих системних викликів, printf() через SemiHosting та мінімальну перевірку, чи вони справді працюють, можна отримати тут.
Кілька слів про його організацію. Є кілька рішень, які б не хотілося приймати раз і назавжди. Тому, реалізував їх вибір з допомогою умовної компіляції. Для порядку, відповідні макроси виніс в runtime_config.h. Цей же файл в майбутньому використовуватиметься і для конфігурації різних аспектів поведінки С++ підтримки часу виконання.
В прикладі використовуються такі макроси:
Як би там не було, такий код цілком коректно працює:
Правда, підключення printf() і компанії вимагає два десятки кілобайт прошивки (Newlib, Newlib-nano із підтримкою floating point, без неї, всього п'ять), але то одноразові витрати -- в неї включається код, котрий реалізовує відповідну функціональність.
Наступного разу подивимося, що робити, коли ціла стандартна бібліотека С не потрібна, достатньо лише (урізаного) printf() поверх SemiHosting, а поки:
__attribute__ ((used)) int _isatty(int file) { return 1; }
Якщо б (ті чи інші) файли (як мінімум -- файлові дескриптори) таки підтримувалися, вона могла б виглядати якось так:
__attribute__ ((used)) int _isatty(int file) { { if ((file == STDOUT_FILENO) || (file == STDERR_FILENO)) { return 1; } else { errno = EBADF; return -1; } } }
Це ж стосується інших пов'язаних функцій.
_lseek()
Переміщення файлового вказівника, повертає його нове положення відносно початку файлу. Див., наприклад, "lseek - move the read/write file offset". Будемо вважати, що по нашому потоку соватися не можна:
__attribute__ ((used)) int _lseek(int file, int ptr, int dir) { return 0; }
Точніше, можна -- це не є помилковою ситуацією (в якій ф-ція мала б повернути -1 і встановити errno), просто далі початку не рухаємося.
_read()
Читає вказану кількість байтів із файлу, повертає, скільки реально прочитано. Детальніше див., наприклад: "read(3) - Linux man page". Вважаємо, що не маємо звідки читати:
__attribute__ ((used)) int _read(int file, char *ptr, int len) { return 0; }
Якщо хотілося б додати можливість читати, скажімо, із UART, можна написати щось таке:
__attribute__ ((used)) int _read(int file, char *ptr, int len) { int i; (void)file; // Ігноруємо номер файлу -- в припущенні, що у нас тільки один UART for(i = 0; i < len; i++) { // Нехай UART_GetChar --- ф-ція, що читає один символ із UART *ptr++ = UART_GetChar(); } return len; }
_write() і SemiHosting
Записує вказану кількість символів у файл, повертає, скільки реально записано. Детальніше, див., наприклад, "write(3) - Linux man page". Звичайно, в дусі мінімалізму, що господарює вище, можна б було просто повертати 0. Однак, нам би хотілося мати можливість якогось більш звичного "feedback" від демоплати, ніж різноманітні мигання світлодіодами (занудно вважаючи, що постійно запалений світлодіод -- це мигання із безмежним періодом :-).
Контролери сімейства STM32 володіють зручним вбудованим засобом для цього --- SemiHosting. Зокрема, цей засіб дозволяє пересилати літери відладчику. Детальніше про нього можна почитати тут:
- Офіційна документація ARM: "The semihosting interface" та (обов'язково) посилання там, скажімо, "What is semihosting?" та "Semihosting operations".
- "LPCXpresso Урок 6. Semihosting. Использование printf в отладке"
- "QEMU ARM semihosting"
__attribute__ ((used)) int _write(int file, char *ptr, int len) { (void)file; int txCount; for ( txCount = 0; txCount < len; txCount++) { SH_SendChar(ptr[txCount]); } return len; }
Де SH_SendChar() --- функція, оголошена в semihosting.h, котра передає один символ дебаггеру. (Звичайно, замість неї може бути яка-небудь UART_PutChar(), для передачі з використанням UART). Її реалізація знаходиться в semihosting.c, думаю, вартує на неї глянути:
static char g_buf[16]; static char g_buf_len = 0; /**************************************************************************//** * @brief Transmit a char on semihosting mode. * * @param ch is the char that to send. * * @return Character to write. *****************************************************************************/ 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; } } }
Зауважте внутрішню буферизацію --- поки буфер не закінчився, або не передано нульовий символ чи символ кінця рядка, нічого не пересилатиметься.
Ось, власне і все --- після реалізації перерахованих вище функцій, printf() працюватиме:
errno
Newlib всередині, замість змінної, використовує макрос errno, для спрощення роботи реентрантних функцій. Однак, для функцій, що реалізовують системні виклики, цей підхід не працюватиме. Тому доводиться в syscalls.c викручуватися так:
#undef errno extern int errno;
Див. також "Error Handling" із "Howto: Porting newlib. A Simple Guide" від Jeremy Bennett, Embecosm та відповідну частину офіційної документації. Взагалі, ці два джерела стосуються практично всіх розглянутих функцій та змінних.
environ
Вказівник на масив вказівників на стрічки із змінними середовища, який закінчується порожньою С-стрічкою. В найпростішому варіанті -- порожній, тобто містить лише згадану порожню стрічку (вказівник на неї):
char *__env[1] = { 0 }; char **environ = __env;
_exit()
Негайно завершити програму, не здійснюючи жодних спроб прибрати за собою. Контролер в такій ситуації може хіба зависнути, увійшовши в нескінчений цикл.
__attribute__ ((used)) void _exit(int rc) { while(1){} }
_execve()
Виконати програму (зазвичай -- замістивши нею процес, створений за допомогою fork()). Детальніше, про те, що це за виклик і як має поводитися, див. "execve - execute program". Підтримки запуску процесів у нас немає:
__attribute__ ((used)) int _execve (char *name, char **argv, char **env) { errno = ENOMEM; return -1; /* Always fails */ }
_fork()
Створити новий процес, на початку -- копію поточного. Детальніше див. "fork(2) - Linux man page". Підтримки "розмноження" процесів у нас немає:
__attribute__ ((used)) int _fork () { errno = EAGAIN; return -1; /* Always fails */ }
_getpid()
Отримати PID (Process ID) поточного процесу. Можна вважати, що процес у нас один:
__attribute__ ((used)) int _getpid () { return 1; }
_kill()
Послати сигнал процесу. (Насправді, не зважаючи на назву, вона далеко не завжди вбиває отримувача, подробиці див., наприклад, тут: "kill(2) - Linux man page"). За відсутності потенціальних отримувачів, реалізація проста:
__attribute__ ((used)) int _kill (int pid, int sig) { errno = EINVAL; return -1; /* Always fails */ }
_link()
Перейменувати файл. За відсутності підтримки файлів:
__attribute__ ((used)) int _link(char *old, char *new) { errno = EMLINK; return -1; }
_open()
Відкрити файл. За відсутності підтримки файлів:
__attribute__ ((used)) int _open (const char *name, int flags, int mode) { errno = ENOSYS; return -1; /* Always fails */ }
_times()
Повертає різноманітні часи процесу -- скільки і як виконувався (скільки власне сам процес, скільки часу провів у системних викликах, скільки часу у дочірніх процесах, детальніше див. тут). Певна осмислена реалізація цілком може існувати навіть у нашому, мінімалістичному випадку, але поки обмежимося відмовою повідомляти цю інформацію:
__attribute__ ((used)) int _times (struct tms *buf) { errno = EACCES; return -1; }
_unlink
Видаляє файл. За відсутності підтримки файлів:
__attribute__ ((used)) int _unlink (char *name) { errno = ENOENT; return -1; /* Always fails */ }
_wait
Очікувати на дочірній процес. За відсутністю таких:
__attribute__ ((used)) int _wait (int *status) { errno = ECHILD; return -1; /* Always fails */ }
Ось і все.
УВАГА! Вище, в педагогічних цілях, наведено повний комплект функцій, що реалізовують системні виклики, необхідні Newlib. Однак, включення тих із них, що непотрібні, особливо із атрибутом used, може привести до марної витрати пам'яті програм (flash-пам'яті) мікроконтролера (може не приводити, особливо з LTO). Тому, в проекті варто залишати лише необхідні.
Аналогічно, хоча послідовна робота із errno важлива, якщо ним не користуються, відповідні рядки коду можна прибрати, зекономивши десятки байт прошивки. З іншого боку, передбачуваність поведінки стандартної бібліотеки, відповідність її очікуванням, теж важлива.
Нотатки щодо готового проекту
Як би там не було, готовий проект, що демонструє описане в цьому пості, тобто реалізацію цих системних викликів, printf() через SemiHosting та мінімальну перевірку, чи вони справді працюють, можна отримати тут.
Кілька слів про його організацію. Є кілька рішень, які б не хотілося приймати раз і назавжди. Тому, реалізував їх вибір з допомогою умовної компіляції. Для порядку, відповідні макроси виніс в runtime_config.h. Цей же файл в майбутньому використовуватиметься і для конфігурації різних аспектів поведінки С++ підтримки часу виконання.
В прикладі використовуються такі макроси:
- PRINT_MESSAGE_ABOUT_HEAP_AND_STACK_COLLISION --- вже траплялося вище, вирішує, повідомляти про те, що sbrk() "вперся" в стек, чи ні.
- PRINT_MESSAGES_ABOUT_SHUTDOWN -- чи друкувати повідомлення про завершення, у функції abort().
- DEFINE_ABORT_FN -- чи взагалі включати реалізацію abort(). (Див. наступні пости -- іноді вона заважає).
- PRINT_MEMORY_LEFT_AFTER_SBRK -- макрос для відладки, вказує, чи друкувати залишок пам'яті після виклику sbrk().
- ALLOW_CBRK_TO_RETURN_LESS_MEMORY_THAN_REQUESTED -- Увага! Макрос вмикає доволі дискусійну можливість! Іноді, наприклад, стандартна бібліотека С++, пробує виділити трішки більше пам'яті, ніж є, не може, і програма аварійно зупиняється. Експерименти показали, що якщо дозволити щоб у таких ситуаціях sbrk() повертав всю наявну пам'ять, але не більше -- код (позірно) працює коректно, аж до коректного повідомлення про недостачу пам'яті, коли malloc чи new (в C++) намагається звернутися до тієї ділянки, яка мала б бути виділена, якщо б sbrk() зміг виділити всю замовлену пам'ять. Однак, вони нічого не доводять -- де-факто цей системний виклик не має так поводитися, тому можливі помилки, як в відмінних від випробуваних ситуаціях, так і з новими версіями компілятора. На ваш страх і ризик, хоча для мене -- окупилося (якщо пам'ятати про "зону ризику").
Як би там не було, такий код цілком коректно працює:
printf("Just text\n"); printf("Integer: %i, hex integer: %x\n", 16, 16); printf("Floating point, %%f: %f; %%g: %g; %%e: %e\n", 1.23, 1.23, 1.23); // Манія величі! :-) Але компілюється. FILE *fp; fp=fopen("c:\\test.txt", "w"); if(fp) { fprintf(fp, "Testing...\n"); }else { printf("Error openin file, errno: %i, text of error: %s\n", errno, strerror(errno) ); }
Правда, підключення printf() і компанії вимагає два десятки кілобайт прошивки (Newlib, Newlib-nano із підтримкою floating point, без неї, всього п'ять), але то одноразові витрати -- в неї включається код, котрий реалізовує відповідну функціональність.
Наступного разу подивимося, що робити, коли ціла стандартна бібліотека С не потрібна, достатньо лише (урізаного) printf() поверх SemiHosting, а поки:
Дякую за увагу!
Немає коментарів:
Дописати коментар