Global assembly, and how to use an @extern to access asm label?

For my PinePhone SoC project I’m accumulating a growing collection of functions now using global assembly, and it is working great :slight_smile: especially now that I am over the 0.15.1 objCopy hurdles :wink:

To organise things better, I have split them into a few files. Is this going to cause problems? in terms of things not being visible if they are assembled out of order? ie will it work better as a single giant assembly file?

...externs misc...
const GASM_UTILS = @embedFile("misc.s");

...externs vector...
const GASM_VECTOR = @embedFile("vector.s");

comptime {
    asm(GASM_UART);
    asm(GASM_UTILS);
    asm(GASM_VECTOR);
    ...
}

I use a linker directive for positioning the only critical __start kernel header block. But what other impact could this have?

Whilst I am here, global assembly functions are working good for me, but I’m wondering how to access a global assembly .globl symbol just as a regular var in Zig please?

So for example if I want to access my ‘.globl .vector’ address, I’m guessing similar to how I access register, maybe along the lines of:

extern const gaVectorAddress: *volatile u32 = ??? what magic syntax do I need here please?

Global assembly is processed in lexical order, so this should work as you expect it to.

But having said that:

Either extern var or the @extern builtin.

hehe, I’m not having much luck with Zig this year, first my freestanding wasm with C was removed, then the chainsaw to std lib, and last week objCopy, and now my favorite feature global assembly :frowning:

It seems to have been around from the start, and yet from reading the link it took only 6 days to decide to remove it, wow, that makes me nervous to use any feature in earnest!

I know that we are not 1.0 yet, but crikey, you all work so fast, and the pace of change seems to only increase! I have to once again fire my local Qwen3 helper.

So anyway, please can you show me examples of the keywords you mentioned, maybe just accessing an asm label please. I cant find anything online or documented. Thanks.

I got a bit further, but the value I get back is not what I stored into VBAR_EL2 system register (includes ISB). Which might actually be my problem and why I try to check the address :slight_smile:

Can someone check the syntax for me please? My asm label is now just vectorTable (since I wasnt sure if my dot prefix (.vector) was the issue)

pub const externLabelTest: *volatile u32 = @extern(*u32, .{.name = "vectorTable",});

To be fair, inline/global assembly in its current form has always been noted as being experimental in the language reference. Inline assembly was added before I got involved with Zig and I obviously can’t speak for Andrew, but I would guess that it was always his intent to replace it with something more language-integrated.

In any case, as mentioned in that proposal, the intent is that the language should have mechanisms to adequately replace any use of global assembly. So if you have a use case that you feel is not addressed by that proposal (and the ones linked to it), please do comment on the issue with details.

1 Like

I’d still appreciate help getting this @extern stuff to work please? I only found a single example after a couple hours searching, and adapted it wrongly it seems.

Regarding github, I’m afraid I deleted my account since they changed their policies this year. I like to make life hard for myself and protest in the only way I can :wink:

It is 35 years since I was last doing any heavy assembly, so it is tough to know what I’ll need for this project. These multi-core SoC things are beasts :slight_smile: but I’m full time on it now, hopefully for the next 12 months.

Already I can see I am going to need some assembly as I move down from EL2 to having users apps running at EL0.

Then I’m gonna need a load of mbox stuff between cores. But I have to investigate what’s involved there. It should be easier once I have these interrupts working.

Also I’m currently using this as my frame buffer:

export var fb0 = [_]u32{0x80FF0000} ** (displayEngine.PP_PIXELS);

but the performance is pretty poor blitting and poking individual pixels. So imagine I’ll need some raw assembly there too.

I wouldnt like to invest in the inline assembly, especially because that is also experimental, and may have it’s own dialect soon. So for me I find it much simpler and easier on the brain just to have some nice easy to read raw asm files:

PS I did find one slight disadvantage to splitting the global asm, compile errors dont know which file it came from.

So I still cannot get a sensible value from @extern, but I realised I can just add an assembly call to fetch the asm label address, and the good news is that the address is correct, phew!

Of course I’d still like to understand why @extern gives a different address please?

extern var a: T means that a is an alias for the symbol a:

  • a will return the value of the symbol a
  • &a will return the address of the symbol a.

const b = @extern(*T, .{ .name = "a" }) means that b is a pointer to the symbol a, not an alias for it:

  • b will return the address of the symbol a (not the value of a).
  • &b will return the address of b (not the address of a).
  • b.* will return the value of the symbol a.

If the symbol represents a pointer and not a value, it is critical that you take into account the additional level of indirection introduced by @extern(), which extern doesn’t. For that reason, it can be a good idea to prefer extern when possible.

Example (x86-64):

comptime {
    asm (
        \\    .data
        \\    .globl foo
        \\    .align 4
        \\foo:
        \\    .long 123
        \\
        \\    .text
        \\    .globl getFooPtr
        \\getFooPtr:
        \\    lea foo(%rip),%rax
        \\    ret
        \\
        \\    .text
        \\    .globl getFoo
        \\getFoo:
        \\    mov foo(%rip),%eax
        \\    ret
        \\
        \\    .text
        \\    .globl setFoo
        \\setFoo:
        \\    mov %ecx,foo(%rip)
        \\    ret
    );
}

Using extern:

const std = @import("std");

extern var foo: i32;
extern fn getFooPtr() *i32;
extern fn getFoo() i32;
extern fn setFoo(value: i32) void;

test {
    try std.testing.expectEqual(&foo, getFooPtr());

    try std.testing.expectEqual(123, foo);
    try std.testing.expectEqual(123, getFoo());
    try std.testing.expectEqual(123, getFooPtr().*);

    foo = 456;

    try std.testing.expectEqual(456, foo);
    try std.testing.expectEqual(456, getFoo());
    try std.testing.expectEqual(456, getFooPtr().*);

    setFoo(789);

    try std.testing.expectEqual(789, foo);
    try std.testing.expectEqual(789, getFoo());
    try std.testing.expectEqual(789, getFooPtr().*);
}

Using @extern():

const std = @import("std");

const foo = @extern(*i32, .{ .name = "foo" });
const getFooPtr = @extern(*const fn () callconv(.c) *i32, .{ .name = "getFooPtr" });
const getFoo = @extern(*const fn () callconv(.c) i32, .{ .name = "getFoo" });
const setFoo = @extern(*const fn (value: i32) callconv(.c) void, .{ .name = "setFoo" });

test {
    try std.testing.expectEqual(foo, getFooPtr());

    try std.testing.expectEqual(123, foo.*);
    try std.testing.expectEqual(123, getFoo());
    try std.testing.expectEqual(123, getFooPtr().*);

    foo.* = 456;

    try std.testing.expectEqual(456, foo.*);
    try std.testing.expectEqual(456, getFoo());
    try std.testing.expectEqual(456, getFooPtr().*);

    setFoo(789);

    try std.testing.expectEqual(789, foo.*);
    try std.testing.expectEqual(789, getFoo());
    try std.testing.expectEqual(789, getFooPtr().*);
}

Take note of how &foo vs foo vs foo.* are used.

4 Likes

Hah, brings back nightmares of my early b*tree C pointer-pointer days :slight_smile:

So understanding that they both only give the value at the label was the clarity I was missing. And I never would have worked out the extra level of indirection for @extern. Thank-you so much for taking the time to provide a very detailed and yet concise explanation. You should takeover the language documentation :slight_smile:

Getting them both into u32 was a bit ugly:

 @truncate(@intFromPtr(&(vectorTableAtExtern.*))));
 @truncate(@intFromPtr(&vectorTableExtern)));

but now all values match, and I can debug the next step in confidence. Cheers!