Using libc functions on bare metal RISC-V

Hi, I’m compiling some bare metal code for RISC-V something like this (a bit cut down from my real code):


const std = @import("std");
const Target = @import("std").Target;

// Construct the build graph in `b` (this doesn't actually build anything itself).
pub fn build(b: *std.Build) void {
    const features = Target.riscv.Feature;
    var cpu_features = Target.Cpu.Feature.Set.empty;

    cpu_features.addFeature(@intFromEnum(features.i));
    cpu_features.addFeature(@intFromEnum(features.m));
    cpu_features.addFeature(@intFromEnum(features.a));
    cpu_features.addFeature(@intFromEnum(features.f));
    cpu_features.addFeature(@intFromEnum(features.zifencei));
    cpu_features.addFeature(@intFromEnum(features.zicsr));
    // cpu_features.addFeature(@intFromEnum(features.d));

    const target = b.resolveTargetQuery(.{
        .cpu_arch = .riscv32,
        // Note, this is really the default Environment (usually the
        // 4th component of the target triple). Calling it the ABI is wrong.
        .abi = .none,
        .os_tag = .freestanding,
        .cpu_features_add = cpu_features,
    });

    const optimize = b.standardOptimizeOption(.{});

    // Module for runtime support (crt0.S etc).
    const runtime_mod = b.createModule(.{
        // `root_source_file` is the Zig "entry point" of the module. If a module
        // only contains e.g. external object files, you can make this `null`.
        // In this case the main source file is merely a path, however, in more
        // complicated build scripts, this could be a generated file.
        .root_source_file = b.path("src/runtime.zig"),
        .target = target,
        .optimize = optimize,
    });

    runtime_mod.addCSourceFile(.{ .file = b.path("src/crt0.S") });

    const exe_mod = b.createModule(.{
        .root_source_file = b.path("src/c_test_root.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    });

    exe_mod.addCSourceFile(.{ .file = b.path("src/my_test.c") });

    exe_mod.addImport("runtime", runtime_mod);

    const exe = b.addExecutable(.{
        .name = "my_test",
        .root_module = exe_mod,
    });

    exe.setLinkerScript(b.path("src/link.ld"));

    b.installArtifact(exe);
}

This works, except if I try to #include <stdio.h> (or other standard headers) in my C file. Then I get:

/.../my_test.c:3:10: error: 'stdio.h' file not found
#include <stdio.h>
         ^~~~~~~~~~

If you do this with the normal RISC-V GCC it works fine, I assume because it provides Newlib as a C library for bare metal applications. It seems like Newlib assumes the Linux syscall ABI so it will happily compile printf(""); and you’ll end up with ecalls to the Linux write() syscall using the Linux syscall numbers. Bit weird but whatever - it works fine and you can implement whatever syscalls you need in crt0.S (or I think you could use Proxy Kernel but I’ve never tried).

Anyway, I guess Zig is doing the “correct” thing here and not pretending there’s a libc when there isn’t one… but that’s quite annoying because it means you can’t even use functions like sprintf() which actually work fine without any support from the execution environment.

Is there any way around this?

Actually I said it works fine if you don’t #include any libc headers. That’s a lie, if you just have int main() { return 0; } it gives this error:

❯ zig build
install
└─ install test_hello_worldc.elf
   └─ zig build-exe test_hello_worldc.elf Debug riscv32-freestanding-none 1 errors
error: libc not available
    note: run 'zig libc -h' to learn about libc installations
    note: run 'zig targets' to see the targets for which zig can always provide libc

libc is mostly syscall wrappers + some other things.
So libc doesn’t work on bare metal, at least not the common ones that zig ships with.
as the error states libc not available.

You will need to either implement the functionality yourself or find a zig or c library that does it for you

Ok but say I download newlib and compile/link with it. How do I actually tell Zig I have libc now? I got the libc not available error when I didn’t even use libc.

Wait, scratch that. I forgot I added .link_libc = true. If I remove that it compiles the libc-free version fine.

I have a bunch of libc functions in a library I use to help porting C code to zig freestanding targets. GitHub - ringtailsoftware/zeptolibc: Some basic libc functions for working with C code in Zig

you link it like any other arbitrary dependency instead of using .link_libc = true