LINUX.ORG.RU

Как правильно использовать C без стандартной библиотеки?

 ,


1

1

Хочу писать на C для МК, но так, чтобы ни единого байта не было в бинарнике, не от моего кода.

В целом я понимаю, как это всё собрать и слинковать. Вопрос больше в том, что компилятор C в моём понимании генерирует код, рассчитывая на определённый рантайм, который я у него хочу забрать. К примеру я слыхал, что компилятор может генерировать вызовы функций вроде memcpy, если посчитает нужным. Также я не очень хорошо представляю, что происходит до вызова main и насколько это важно.

Также развитие этого вопроса - тот же вопрос про C++.

★★★★

Под мирокронтроллеры либо спецсборки компиляторов либо спец флаги targets всякие, не подключай никаких заголовков и не будет у тебя ничего левого. Твой код будет чист,красив,собран но неработающий. В ином случае тебе не то что даташит, тебе спеку на твой микроконтроллер учить придётся =)

И опять же микроконтроллеры бывают разные я сомневаюсь что какойнить STM32 можно запрограммировать прям чисто снуля, AVR вот можно, хоть в блокноте машкоды пиши и всё.

LINUX-ORG-RU ★★★★★
()

До вызова main происходит переход по метке _start (можешь её объявить как функцию void start(void)), где libc запускает статические инициализаторы и готовит аргументы для main().

memcpy компилятор сам не вставит никуда. Наоборот, он вызовы memcpy может превратить в ассемблерную реализацию на месте.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от t184256

компилятор C, да ещё с -nostdlib

Это опция линкера
Пока до него дойдёт, уже всё вставлено будет 🤡
(Ладно, gcc, как драйвер, мог бы реагировать на -nostdlib даже при вызове компилятора, но он этого не делает)

Ну и где

Да вот https://godbolt.org/z/v6ceh67fo 🤡

utf8nowhere ★★★
()
Последнее исправление: utf8nowhere (всего исправлений: 1)

так, чтобы ни единого байта не было в бинарнике, не от моего кода.

BITS 32;
ORG 05430000h;

DB 0x7F, "ELF";
DD 01h, 00h, $$;
DW 02h, 03h;
DD @main;
DW @main - $$;

@main:
  INC EBX;
  DB 05h; <-- ADD EAX,
  DD 04h; <-- LONG(04h)
  MOV ECX, @text;
  MOV DL, 12;
  INT 80h;
  AND EAX, 00010020h;
  XCHG EAX, EBX;
  INT 80h;

@text:
  DB "ELF, hello!", 0Ah;

nasm -f bin, на выходе бинарь размером 61 байт

и все было бы совсем клево, тока это уже не с(++)

olelookoe ★★★
()

Например, некоторые линкеры поддерживают ключик -nostdlib. А так тебе надо будет написать свой стартап код. Включая например, обнуление памяти, копирование инициализированных переменных из ROM в RAM итд

vromanov ★★★
()

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

далее зависит от библиотечных функций, тех же memcpy, которые ты всовываешь в проект.

читай документацию - смотри листинги компиляции.

pfg ★★★★★
()
Последнее исправление: pfg (всего исправлений: 1)
Ответ на: комментарий от DrBrown

Я и пишу на ассемблере, но С кажется немного удобней, поэтому хочу его освоить следующим шагом. Но хочется пока сохранить контроль над результатом.

vbr ★★★★
() автор топика
Последнее исправление: vbr (всего исправлений: 1)
Ответ на: комментарий от vromanov

А так тебе надо будет написать свой стартап код. Включая например, обнуление памяти, копирование инициализированных переменных из ROM в RAM итд

Про это хотелось бы поподробней. Какую именно память надо обнулить? Что именно копировать? Не очень понятно. Откуда стартап код это может знать? Я понимаю, о чём ты говоришь (глобальные переменные и тд), но пока не понимаю, как предлагается это реализовать. Наверное компилятор должен сам генерировать код для инициализации, а мне надо только вызвать какие-то функции-конструкторы? Как их найти?

vbr ★★★★
() автор топика
Последнее исправление: vbr (всего исправлений: 2)
Ответ на: комментарий от vbr

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

pfg ★★★★★
()

без вопросов к общественности это делается так.

поставь ему -nostdlib и билди. если он что не найдет - линкер будет ругаться и орать что ему нужен такой-то символ. то есть код такой вот функции. сделай файл my_stdlib.c, и напиши там код этих функций ручками, там тривиальные функции будут. как только все слинкуется нормально - попробуй пустить. должно работать. еще надо код стартапа на асме, но он короткий

можешь попробовать сбилдить хелловорд с -nostdlib, и увидишь что ему не хватает.

я точно сишный рантайм не знаю, я так делал для плюсов. у них рантаймы разные, и все работало нормально естессно.

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 2)
Ответ на: комментарий от vbr

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

vromanov ★★★
()
Ответ на: комментарий от vromanov

Напрмиер, тебе надо обнулить память для статических переменных, установить указатель стека итд. Все это делает стартап код

он же на асме пишет. у него свой стартап есть. иначе он прогу не запустит. он может и своим стартапом пустить сишный мейн по идее.

и вообще это стопицот раз описано в энторнетах, как подсунуть си или плюсовой проге свой stdlib.

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)

Возможно, это поможет. На RISC-V я писал сначала на асме, потом перешел на Си. Стартап остается асмовский.

Ключи компилятора -static -fno-plt -fno-pic -Wl,--build-id=none -nostdlib. Какие из них специфичны именно для моего случая не уверен.

Хочу писать на C для МК

Для какого МК?

Также я не очень хорошо представляю, что происходит до вызова main и насколько это важно.

Много чего интересного происходит. Инициализация стека, базовой периферии, запись нулей в .bss, запись значений по умолчанию. Ну и вызов main, конечно.

COKPOWEHEU
()

У компиляторов есть опции для отключения использования стандартных функций, так же есть -ffreestanding, которая заставит не полагаться на поведение стандартных функций при оптимизации.
Но это всё ломает оптимизации, потому рекомендуется реализовывать нужные функции. К примеру пара sin + cos заменяется компилятором на sincos т.к их можно посчитать вместе быстрее. Соответственно отключение этой оптимизации приведёт к необходимости двойного вычисления

mittorn ★★★★★
()
Ответ на: комментарий от t184256

Так делали компиляторы даже 25 лет назад. Системный memcpy надежнее, чем то что программист там себе напишет. :)

А вот чтобы так не делал, в GCC завели флаг -ffreestanding. Или что бы отключать по одиночке: -fno-builtin-имяфункции Это понятие даже в стандарте есть.

@vbr, это не та вещь которой ты хочешь заниматься. Хотя если для обучения – сойдёт. Но там лучше сразу свой загрузчик или ядро ОС писать. :)

a1ba ★★
()
Последнее исправление: a1ba (всего исправлений: 2)

Нужна опция линковки -nostdlib, свои реализации функций стандартной библиотеки, которые не найдет линовщик.

Если хочешь, чтоб прям совсем всё своё, то добавь к этому свой cstartup и свой скрипт лилковки.

Напрмер, я когда-то написаk на основе ARM-овского стартапа вот что-то такое:

/* Copyright (c) 2011 - 2014 ARM LIMITED
   All rights reserved.
   Redistribution and use in source and binary forms, with or without
   modification, are permitted provided that the following conditions are met:
   - Redistributions of source code must retain the above copyright
     notice, this list of conditions and the following disclaimer.
   - Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in the
     documentation and/or other materials provided with the distribution.
   - Neither the name of ARM nor the names of its contributors may be used
     to endorse or promote products derived from this software without
     specific prior written permission.
   *
   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS AND CONTRIBUTORS BE
   LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
   CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   POSSIBILITY OF SUCH DAMAGE.
   ---------------------------------------------------------------------------*/

#include <stdint.h>

/*Function pointer prototype*/
typedef void (*func_ptr)(void);

/*----------------------------------------------------------------------------*/
/* Linker generated Symbols */
extern uint32_t _sidata; /*.data load addr in flash*/
extern uint32_t _sdata;  /*.data section start in ram*/
extern uint32_t _edata;  /*.data section end in ram*/

extern uint32_t _sbss;   /*.bss section start in ram*/
extern uint32_t _ebss;   /*.bss section end in ram*/
extern uint32_t _estack; /*.stack top*/

/*Constructors and destructors*/
/*Preinit array*/
extern func_ptr __preinit_array_start;
extern func_ptr __preinit_array_end;

/*Init array*/
extern func_ptr __init_array_start;
extern func_ptr __init_array_end;

/*Fini array*/
extern func_ptr __fini_array_start;
extern func_ptr __fini_array_end;

/*----------------------------------------------------------------------------*/
/* External References */
#ifdef CRT_USE_LIBC
extern void __libc_init_array(void);     /*libc constructor*/
#   define _OPT_LIBC_INIT() __libc_init_array()
#else
#   define _OPT_LIBC_INIT() do{}while(0)
#endif
extern void SystemInit(void);            /* CMSIS System Initialization      */
extern void main(void);                  /*The program main*/

/*----------------------------------------------------------------------------*/
/* Internal References */
void Default_Handler(void)__attribute__((naked));    /* Default empty handler */
void Reset_Handler(void)__attribute__((naked));              /* Reset Handler */

/*----------------------------------------------------------------------------*/
/* Interrupt Handlers */
#define XCAT(a,b) a##b
#define DECL_VECTOR(v,p) \
    void XCAT(v,p)(void) __attribute__ ((weak, alias("Default_Handler")));

#define CM4_VECTORS \
XVECTOR(NMI)        \
XVECTOR(HardFault)  \
XVECTOR(MemManage)  \
XVECTOR(BusFault)   \
XVECTOR(UsageFault) \
ZVECTOR(0)          \
ZVECTOR(0)          \
ZVECTOR(0)          \
ZVECTOR(0)          \
XVECTOR(SVC)        \
XVECTOR(DebugMon)   \
ZVECTOR(0)          \
XVECTOR(PendSV)     \
XVECTOR(SysTick)

/*Declare Cortex-M4 core vectors*/
#define XVECTOR(v) DECL_VECTOR(v,_Handler)
#define ZVECTOR(v)
CM4_VECTORS
#undef XVECTOR
#undef ZVECTOR

/*Declare MCU vectors*/
#define XVECTOR(v) DECL_VECTOR(v,_IRQHandler)
#define ZVECTOR(v)
#include <vectors.h>
#undef XVECTOR
#undef ZVECTOR

/*----------------------------------------------------------------------------*/
/*Interrupt Vector table*/
#define VT_ENTRY(v,p) ((func_ptr)(XCAT(v,p))),
#define ZVECTOR(v) ((func_ptr)(v)),

const func_ptr _vectors[] __attribute__ ((section(".isr_vector"))) =
{
    /*Cortex-M4 default vectors*/
    ZVECTOR(&_estack)
    Reset_Handler,
#   define XVECTOR(v) VT_ENTRY(v,_Handler)
    CM4_VECTORS
#   undef XVECTOR

    /*MCU vectors*/
#   define XVECTOR(v) VT_ENTRY(v,_IRQHandler)
#   include <vectors.h>
#   undef XVECTOR
};
#undef ZVECTOR

/*----------------------------------------------------------------------------*/
/*Must be included after all usage of vectors.h!!! */
#include <stm32.h>
/*----------------------------------------------------------------------------*/
/* Reset Handler called on controller reset */
static inline void _init_vars(void)
{
    uint32_t *src, *dst;

    /*.data*/
    src  = &_sidata;
    for (dst = &_sdata; dst < &_edata;)
    {
        *dst++ = *src++;
    }

    /*.bss*/
    for (dst = &_sbss; dst < &_ebss; dst++)
    {
        *dst = 0ul;
    }
}

static inline void _run_the_code(void)
{
    func_ptr * f;

    /*Constructors*/
    for (f = &__preinit_array_start; f < &__preinit_array_end; f++)
    {
        (*f)();
    }

    for (f = &__init_array_start; f < &__init_array_end; f++)
    {
        (*f)();
    }

    /*Call to libc init if needed*/
    _OPT_LIBC_INIT();

    /*App code*/
    SystemInit();
    main();

    /*Destructors*/
    for (f = &__fini_array_start; f < &__fini_array_end; f++)
    {
        (*f)();
    }

    /*The end*/
    while (1)
    {
        /* Set SLEEPDEEP bit of Cortex System Control Register */
        SET_BIT(SCB->SCR, ((uint32_t)SCB_SCR_SLEEPDEEP_Msk));

        /* Request Wait For Interrupt */
        __WFI();
    }
}

void Reset_Handler(void)
{
    /*Set the SP... It's strange but the core doesn't set the SP by itself.*/
    __asm__ __volatile__ (
            "ldr sp, =_estack\n\t"
            "isb             \n\t"
            "dsb             \n\t"
            :::);

    /*Initiate .data and .bss*/
    _init_vars();
    __DSB();
    /*Execute the code*/
    _run_the_code();
}

/*----------------------------------------------------------------------------*/
/* Default Handler for Interrupts */
void Default_Handler(void)
{
    NVIC_SystemReset();
}
shkolnick-kun ★★★★★
()

компилятор может генерировать вызовы функций вроде memcpy,

Добавь в ключи -ffreestanding.

Также я не очень хорошо представляю, что происходит до вызова main и насколько это важно.

До вызова main() вызывается _start, но если ты дёргаешь контроллер, тебя это парить не должно. Тебя должно парить, чтобы твой код начинался с определённого адреса.

hateyoufeel ★★★★★
()
Ответ на: комментарий от firkax

memcpy компилятор сам не вставит никуда. Наоборот, он вызовы memcpy может превратить в ассемблерную реализацию на месте.

Ты опять выходишь на связь? GCC ещё как вставляет, за милую душу!

hateyoufeel ★★★★★
()
Ответ на: комментарий от utf8nowhere

Харош, поймал! :)

Впрочем, у человека в первую очередь вопрос как избавиться от стандартной библиотеки, вот добавить mem* функции можно как раз и самостоятельно. Как и abort(), если трапов не завезли.

А вот кстати что ТС хочет делать с рантаймом типа libgcc или compiler-rt я не знаю, ибо вот его вызовы компилятор может вставить и даже не спросить. И правильно делает, впрочем, у ТС тут какие-то совсем невнятные желания.

a1ba ★★
()
Последнее исправление: a1ba (всего исправлений: 1)

Тебе, во-первых, нужен ключ линкера -nostartfiles. Тогда он не будет брать стартап готовый, а будет ожидать его от тебя.

Если совсем не нужна библиотека, то -nostdlib. Если нужно нормально программировать, но чтоб прошивка была поменьше, то есть --specs=nano.specs. Отличный ключ, кстати.

Beewek ★★
()
Ответ на: комментарий от utf8nowhere

Всё равно нужны memcpy, memmove, memset и memcmp.

Они не понадобятся, разве только при копировании структуры по значению компилятор сам сгенерит memcpy.

Для крестов не обойтись без создания своего хипа в стартапном коде, и функций malloc и free, т.к. плюсовые new и delete их в итоге вызывают. Наверное в т.ч. за такую особенность плюсы и не используют в драйверах ядра, и в МК тоже не надо.

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

Далее можно на Ц. Настроить частоты CPU и шин.

Настроить в GPIO назначение линий, инициализировать контроллер прерываний и установить обработчики. Тогда можно и прерывания разрешать.

Настроить контроллеры устройств, всех что нужны, в т.ч. UART например.

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

bugs-bunny
()
Ответ на: комментарий от bugs-bunny

Они не понадобятся, разве только при копировании структуры по значению компилятор сам сгенерит memcpy.

А при инициалиции структуры нулём компилятор не сгенерит memset?

Вобще не понятно требование ТС:

ни единого байта не было в бинарнике, не от моего кода.

То ли он хочет, чтобы компилятор без отключения оптимизации поменьше использовал memset/memcpy (-fno-tree-loop-distribute-patterns), то ли использовал memcpy, написаный ТС'ом, а не встроеный.

mky ★★★★★
()
Ответ на: комментарий от mky

То ли он хочет, чтобы компилятор без отключения оптимизации поменьше использовал memset/memcpy (-fno-tree-loop-distribute-patterns), то ли использовал memcpy, написаный ТС’ом, а не встроеный.

Не знаю как у ТСа, а у меня в risc-v была проблема, что stdlib для 32-битной архитектуры то ли недоделанный, то ли с багами, в общем, при попытке воспользоваться стандартным gcc-шным окружением оно ругалось на отсутствие тех самых memcpy и компилироваться не желало. Пришлось писать собственный стартап,

COKPOWEHEU
()
Ответ на: комментарий от a1ba

У меня желание понять, что происходит между кодом на С и машинным кодом. Если без libgcc никак, значит буду подключать. Пока вроде ответов хороших много было, буду изучать, всем спасибо за помощь и дискуссию.

vbr ★★★★
() автор топика