zig语言代替C语言进行裸机开发的尝试-2023年笔记

接触rust的时候,无意中认识了zig,目前版本是zig 0.10.0,还没有正式的1.0版本。

初步使用的感受: 1). 用zig写出的代码更防崩,不会像C那样出现很多内存非法访问的情况 (比如这些情形:栈保护、整数溢出、下标越界、OOM、DF、Leak。。。) 2). 拥有“类”语法,相同功能的项目开发过程中,所需编写的总代码量肯定会比C少 3). 比Rust/C++容易学,但是比C稍难学一点点 4). 基于LLVM开发的编译器,天生支持交叉编译多种cpu架构的目标代码。 5). 可与C、asm进行混合开发,兼容C/C++,完全可以拿来当另一个C编译器用 6). 支持编译期代码 7). 可以源码中集成单元测试代码 8). 自带基于自身脚本的构建系统,不需要makefile之类的 9). 支持 try catch exception处理 10). 编译速度快(网传) 11). MIT许可(相当宽松) 12). 原生支持异步

缺点及风险: 1). 小团队开发维护的编程语言,目前网上能看到的只有跟Uber一家有合作。 2). 未成熟、定型,还在变,比如三年前的zig项目,现在已经不能直接编译了。(构建脚本已变) 3). 小众,国内用的人更加少,文档不全,中文的资料更是少得可怜。

安装过程很简单 官网有编译好的,下载解压,添加路径即可。

下载 ⚡Zig Programming Language

只有一个可执行文件:zig 项目创建、构建都是用这个指令。

下面是用zig做aarch64(armv8a)裸机开发的内容,展示了zig的基本用法: 包括基本语法、类的静态成员函数、类实例的成员函数、 与C/asm互调、混合编译、链接、构建脚本、交叉编译、MMIO读写。。。

源码目录结构:

编译指令:

zig build #生成elf格式文件,体积为130Kb上下 zig build bin #生成bin格式文件,40Kb,不调用C的话27Kb左右

在build.zig中将编译模式调整为 "safety off" 后,编译体积甚至比C还要小,bin文件输出不到1kb!

elf.setBuildMode(std.builtin.Mode.ReleaseSmall);//mode);

src/main.zig:

const std = @import("std");

const io = std.io;

const os = std.os;

const a_number: i32 = 1234;

export fn main() void {

const uart = UART.init();

const out = uart.getWriter();

out.print("Hello1, {s}!\r\n", .{"world"}) catch return;

UART.putc(0x63);

UART.putc('a');

UART.putc('\n');

out.print("Hello2, {s} {}==0x{X}!\r\n", .{"世界", a_number, a_number}) catch return;

out.print("Hello3, {}\r\n", .{cEng.c_func(3, 8)}) catch return;

while(true)

{

var c = UART.getc();

out.print("-->{c}!\r\n", .{c}) catch return;

}

out.print("Hello4, {s}!\r\n", .{"world"}) catch return;

}

const UART0DR = @intToPtr(*volatile u8, 0x09000000);

const UART0FR = @intToPtr(*volatile u8, 0x09000018);

pub const UART = struct {

inited: i32 = 0,

pub fn init() UART {

return UART {

.inited = 1,

};

}

pub fn putc(ch:u8) void

{

while ( ((UART0FR.*) & (1 << 5)) != 0 ) {}

UART0DR.* = ch;

}

pub fn getc() u8

{

while ( ((UART0FR.*) & (1 << 4)) != 0 ) {}

var ch = UART0DR.*;

return ch;

}

pub fn doWrite(self:UART, bytes: []const u8) WriteError!usize {

if(self.inited > 0) {}

var i:usize=0;

for (bytes) |c| {

putc(c);

i += 1;

}

return i;

}

pub const WriteError = os.WriteError;

pub const Writer = io.Writer(UART, WriteError, doWrite);

pub fn getWriter(uart: UART) Writer {

return .{ .context = uart };

}

};

export fn zig_func(a: i32, b: i32) i32 {

return a + b;

}

const cEng = @cImport({

@cDefine("ZIG_WITH_C", "1");

@cInclude("coWork.h");

});

src/boot.S:

.section ".text.boot"

.global _start

_start:

mrs x1, mpidr_el1

and x1, x1, #3

cbz x1, 2f

1:

wfe

b 1b

2:

ldr x0, =stack_top

bic sp, x0, #0xf /* 16-byte alignment for ABI compliance */

bl main

bl .

src/linker.ld:

#include

/*

OUTPUT_FORMAT("elf64-littleaarch64", "elf64-littleaarch64", "elf64-littleaarch64")

OUTPUT_ARCH(aarch64)

*/

ENTRY(_start)

SECTIONS {

. = 0x40000000;

. = ALIGN(8);

__text_start = .;

.text :

{

KEEP(*(.text.boot))

STARTOBJ(.text)

*(.text)

}

/* shell_cmd_item 保存在这个段 */

. = ALIGN(8);

__shell_cmd_list_start = .;

.ShellCmdList ALIGN(8) : { *(.ShellCmdList) }

__shell_cmd_list_end = .;

/* resource_item 保存在这个段 */

. = ALIGN(8);

__resource_item_start = .;

.resource_item ALIGN(8) : { *(.ResourceList) }

__resource_item_end = .;

.rodata ALIGN(8) : {*(.rodata*)}

.data ALIGN(8) : { *(.data) }

. = ALIGN(8);

__bss_start = .;

.bss ALIGN(8) : { *(.bss) *(COMMON) }

__bss_end = .;

/* resource 实际保存在这个段 */

. = ALIGN(8);

.extdata ALIGN(8): {*(.extdata*)}

/* 栈内存 */

. = ALIGN(16);

. = . + 1M; /* 4kB of stack memory => 1MB */

stack_top = .;

/DISCARD/ : { *(.dynsym) }

/DISCARD/ : { *(.dynstr*) }

/DISCARD/ : { *(.dynamic*) }

/DISCARD/ : { *(.plt*) }

/DISCARD/ : { *(.interp*) }

/DISCARD/ : { *(.gnu*) }

}

#ifdef TPL

/* tpl 运行在64kb 的iRam 中,体积不能超了,况且iRam分出来4kb做栈内存了,实际可用的内存就更少了,所以定个58kb的可用内存 */

ASSERT( (__bss_start - __text_start) < (58*1024),

"

编译出来的 tpl.bin 太大,iRam 装不下了,请编译为 spl => make spl

");

#endif

src/coWork.h:

int c_func(int a, int b);

src/coWork.c:

extern int zig_func(int a, int b);

int c_func(int a, int b) {

return zig_func(a, a) + b;

}

startQemu.ps1:

[System.Console]::OutputEncoding = [System.Console]::InputEncoding = [System.Text.Encoding]::UTF8

& "d:\Program Files\qemu\qemu-system-aarch64.exe" -nographic -machine virt-6.2,gic-version=3,secure=on,virtualization=on -cpu cortex-a53 -m 1024 -semihosting -kernel ".\zig-out\bin\out.elf"

build.zig:

const Builder = @import("std").build.Builder;

const builtin = @import("builtin");

const std = @import("std");

pub fn build(b: *Builder) void {

const target = .{

.cpu_arch = .aarch64,

.cpu_model = .{ .explicit = &std.Target.aarch64.cpu.cortex_a53 },

.os_tag = .freestanding,

.abi = .none,

};

// Standard release options allow the person running `zig build` to select

// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.

const mode = b.standardReleaseOptions();

const elf = b.addExecutable("out.elf", "src/main.zig");

elf.addAssemblyFile("src/boot.S");

elf.addIncludePath("src/"); // addIncludeDir 已经被官方标为deprecated,不建议使用

elf.addCSourceFile("src/coWork.c", &[_][]const u8{"-std=c99"});

elf.setTarget(target);

elf.setBuildMode(mode);

elf.setLinkerScriptPath(.{ .path = "src/linker.ld" });

// --script

const bin = b.addInstallRaw(elf, "out.bin", .{});

const bin_step = b.step("bin", "Generate binary file to be flashed");

bin_step.dependOn(&bin.step);

b.default_step.dependOn(&elf.step);

b.installArtifact(elf);

}

qemu运行截图:

通过 struct {} 定义类时,大括号里面的成员函数, 如果它的第一个参数的类型是类本身, 则此函数被当成类的实例的成员函数(比如代码中的 uart.getWriter ), 否则被当成类的静态函数,要通过 “类名.函数名的方式调用”,(比如代码中的 UART.putc )

zig 支持的目标cpu架构,可参考此目录:$(zig_install_dir)/lib/std/target/ 也可能通过运行这条指令列出:zig targets

碰到两个坑:

LTO (Link Time Optimization) 默认会启动,编译模式为 release* 时,export linksection 全局常量不会被放入bin文件的指定数据段,即会被“优化掉”,解法是不让它优化:build.zig 里面加上 exe.want_lto = false; 即可。

link.ld 中预留栈空间没有在最尾处,然后llvm又在后面补了got 数据段,导致预留栈段全被0填充,然后生成的bin文件体积变大

参考:GitHub - rbino/zig-stm32-blink: Use Zig to blink some LEDs

Documentation - The Zig Programming Language

Documentation - Zig

0.10.0 Release Notes ⚡ The Zig Programming Language