Skip to content

inline assembly improvements #215

Closed as duplicate
Closed as duplicate

Description

@andrewrk

This inline assembly does exit(0) on x86_64 linux:

    asm volatile ("syscall"
        : [ret] "={rax}" (-> usize)
        : [number] "{rax}" (60),
            [arg1] "{rdi}" (0)
        : "rcx", "r11");

Here are some flaws:

Activity

  1. added
    enhancementSolving this issue will likely involve adding new logic or components to the codebase.
    on Nov 18, 2016
  2. andrewrk commented on Nov 18, 2016

    @andrewrk
    MemberAuthor

    One idea:

    const result = asm volatile ("rax" number: usize, "rdi" arg1: usize, "rcx", "r11")
        -> ("rax" ret: usize)  "syscall" (60, 0);

    This shuffles the syntax around and makes it more like a function call. Clobbers are extra "inputs" that don't have a name and a type. The register names are still clunky.

    This proposal also operates on the assumption that all inline assembly can operate on inputs and outputs.

  3. added this to the 0.1.0 milestone on Nov 18, 2016
  4. andrewrk commented on Nov 18, 2016

    @andrewrk
    MemberAuthor

    @ofelas can I get your opinion on this proposal?

  5. ofelas commented on Nov 18, 2016

    @ofelas

    Right, you really made me thinkg here, haven't done that much asm in zig yet, here are a few that I've used on x86, they primarily struggle with the issue of multiple return values, the below examples may not be correct, I always end up spending some time reading the GCC manuals when doing inline asm in C, it isn't always straight forwards.

    I just skimmed through the discussion over at Rust users and Rust inline assembly, they seem to have similar discussions and it seems that the asm feature may not be used that much. If you really need highly optimized or complex asm wouldn't you break out to asm (or possibly llvm ir)?

    I guess what we have to play with is what LLVM provides, at least as long as zig has a tight connection to it (It seems there are discussions on also supporting Cretonne in Rust according to the LLVM Weekly).

    With the above proposal would I write the PPC eieio (and isync, sync) like this _ = asm volatile () -> () "eieio" (); and old style _ = asm volatile ("eieio");? This may typically be available as an intrinsic barrier, I guess. Think I read somewhere that the _ would be the same as Nims discard, it may not be needed as this asm didn't return anything.

    Not sure I answered you question...

    inline fn rdtsc() -> u64 {
        var low: u32 = undefined;
        var high: u32 = undefined;
        // ouput in eax and edx, could probably movl edx, fingers x'ed...
        low = asm volatile ("rdtsc" : [low] "={eax}" (-> u32));
        high = asm volatile ("movl %%edx,%[high]" : [high] "=r" (-> u32)); 
        ((u64(high) << 32) | (u64(low)))
    }
    

    The above obviously is a kludge, I initially hoped to write it that more like this, it does however feel strange having to specify the outputs twice, both lhs and inside the asm outputs, with the potential of mixing the order which may be important.

    inline fn rdtsc() -> u64 {
        // ouput in eax and edx
        var low: u32 = undefined;
        var high:u32 = undefined;
        low, high = asm
            // no sideeffects
            ("rdtsc"
             : [low] "={eax}" (-> u32), [high] "={edx}" (-> u32)
             : // No inputs
             : // No clobbers
             );
        ((u64(high) << 32) | (u64(low)))
    }
    

    Or possibly like this, not having to undefined/zeroes/0 the output only parameters;

    inline fn rdtsc() -> u64 {
        // ouput in eax and edx
        const (low: u32, high: u32) = asm
            // no sideeffects
            ("rdtsc"
             : [low] "={eax}" (-> u32), [high] "={edx}" (-> u32)
             : // No inputs
             : // No clobbers
             );
        ((u64(high) << 32) | (u64(low)))
    }
    

    I've also tinkered with the cpuid instruction which is particularly nasty;

    inline fn cpuid(f: u32) -> u32 {
        // See: https://en.wikipedia.org/wiki/CPUID, there's a boatload of variations...
        var id: u32 = 0;
        if (f == 0) {
            // Multiple outputs (as an ASCII string) which we mark as clobbered and just leave untouched
            return asm volatile ("cpuid" : [id] "={eax}" (-> u32): [eax] "{eax}" (f) : "ebx", "ecx", "edx");
        } else {
            return asm volatile ("cpuid" : [id] "={eax}" (-> u32): [eax] "{eax}" (f));
        }
    }
    
  6. andrewrk commented on Nov 18, 2016

    @andrewrk
    MemberAuthor

    With the proposal, rdtsc would look like this in zig:

    fn rdtsc() -> u64 {
        const low, const high = asm () -> ("eax" low: u32, "edx" high: u32) "rdtsc" ();
        ((u64(high) << 32) | (u64(low)))
    }

    This seems like an improvement.

    cpuid with the proposal. I propose that instead of naming the function after the assembly instruction, we name it after the information we want. So let's choose one of the use cases, get vendor id.

    fn vendorId() -> (result: [12]u8) {
        const a: &u32 = (&u32)(&result[0 * @sizeOf(u32)]);
        const b: &u32 = (&u32)(&result[1 * @sizeOf(u32)]);
        const c: &u32 = (&u32)(&result[2 * @sizeOf(u32)]);
       *a, *b, *c = asm () -> ("ebx" a: u32, "ecx" b: u32, "edx" c: u32) "cpuid" ();
    }

    Once again volatile not necessary here. cpuid doesn't have side effects, we only want to extract information from the assembly.

    So far, so good. Any more use cases?

  7. ofelas commented on Nov 18, 2016

    @ofelas

    Yes, that ain't too shabby, so with the correct input in eax it is;

    fn vendorId() -> (result: [12]u8) {
        const a: &u32 = (&u32)(&result[0 * @sizeOf(u32)]);
        const b: &u32 = (&u32)(&result[1 * @sizeOf(u32)]);
        const c: &u32 = (&u32)(&result[2 * @sizeOf(u32)]);
       // in eax=0, out: eax=max accepted eax value(clobbered/ignored), string in ebx, ecx, edx
       *a, *b, *c = asm ("eax" func: u32) -> ("ebx" a: u32, "ecx" b: u32, "edx" c: u32, "eax") "cpuid" (0);
    }
    

    Would something like this be possible, ignoring my formatting?

    result = asm ( // inputs
            "=r" cnt: usize = count,
            "=r" lhs: usize = &left,
            "=r" rhs: usize = &right,
            "=r" res: u8 = result,
            // clobbers
            "al", "rcx", "cc")
            -> ( // outputs
            "=r" res)
            // multiline asm string
            \\movq %[count], %rcx
            \\1:
            \\movb -1(%[lhs], %rcx, 1), %al
            \\xorb -1(%[rhs], %rcx, 1), %al
            \\orb %al, %[res]
            \\decq %rcx
            \\jnz 1b
            // args/parameters
            (count, &left, &right, result);
    
  8. andrewrk commented on Nov 19, 2016

    @andrewrk
    MemberAuthor

    Yes, that ain't too shabby, so with the correct input in eax it is;

    Ah right, nice catch.

    I like putting the values of the inputs above as you did. Then we don't need them below.

    Is the count arg necessary to have the movq instruction? seems like we could pass that as a register.

    And then finally result should be an output instead of an input right?

    So it would look like this:

    const result = asm ( // inputs
            "{rcx}" cnt: usize = count,
            "=r" lhs: usize = &left,
            "=r" rhs: usize = &right,
            // clobbers
            "al", "rcx", "cc")
            -> ( // outputs
            "=r" res: u8)
            // multiline asm string
            \\1b:
            \\movb -1(%[lhs], %rcx, 1), %al
            \\xorb -1(%[rhs], %rcx, 1), %al
            \\orb %al, %[res]
            \\decq %rcx
            \\jnz 1b
    );

    This is a good example of why we should retain the constraint syntax, since we might want {rcx} or =r.

  9. ofelas commented on Nov 19, 2016

    @ofelas

    Not too familiar with the x86 asm, I nicked that example from the Rust discussions, in this case rcx (and ecx i 32 bit) is a loop counter somewhat similar to ctr on Power PC. So the movq, decq, jnz drives the loop. So as long at that condition is met it probably doesn't matter. Maybe it could have been done with the loop instruction that decrements and tests at the same time.

    result is both an input and an output, like if you were updating a cksum or similar where you would feed in an initial or intermediate value that you want to update.

    Are you planning to support all the various architecture specific input/output/clobber constraints and indirect inputs/outputs present in LLVM?

  10. kiljacken commented on Dec 9, 2016

    @kiljacken

    Another avenue to go down is the MSVC way of doing inline assembly. M$ does a smart augmented assembly, where you can transparently access C/C++ variables from the assembly. An example would be a memcpy implementation:

    void
    CopyMemory(u8* Dst, u8* Src, memory_index Length)
    {
    	__asm {
    		mov rsi, Src
    		mov rdi, Dst
    		mov rcx, Length
    		rep movsb
    	}
    }

    It provides a really nice experience. However, MSVC isn't smart about the registers, so all registers used are backed up to the stack before emitting the assembly, and are then restored after the assembly. This avoids the mess of having to specify cluttered registers, but at the cost of a fair bit of performance.

    The smart syntax is awesome, but it might be hard fit with a LLVM backend, if you do not want to write an entire assembler as well.

  11. modified the milestones: 0.2.0, 0.1.0 on Apr 21, 2017
  12. dd86k commented on Oct 19, 2017

    @dd86k

    As kiljacken says, I personally really, really enjoy the Intel syntax over GAS as D has done it (except for GDC, which is based on GCC). I'm only assuming it'll be harder to implement a MSVC-styled inline assembly feature.

  13. andrewrk commented on Oct 19, 2017

    @andrewrk
    MemberAuthor

    The end game is we will have our own assembly syntax, like D, which will end up being compiled to llvm compatible syntax. It's just a lot of work.

    I at first tried to use the Intel syntax but llvm support for it is buggy and some of the instructions are messed up to the point of having silent bugs.

  14. removed this from the 0.2.0 milestone on Oct 19, 2017
  15. 50 remaining items

  16. lerno commented on Jun 17, 2023

    @lerno

    The question is "what is robust" though. It is a well known problem that string based inline asm has zero checks that it actually uses the correct clobbers etc. At least in my research I found pages discussing bugs in asm blocks due to people either misunderstanding the constraints or mistakenly forgetting about some.

    The problem here of course being that they're only observable in specific scenarios, so it would occur almost randomly depending on the final register allocation and code ordering.

    So one also has to ask oneself: is manual clobbering LESS or MORE likely to be correct than automatic clobbering? If one is worried about things like INT21, then perhaps start with a pessimistic clobber, allowing the user to ease the restrictions. This is something that should be discussed.

  17. eLeCtrOssSnake commented on Jun 17, 2023

    @eLeCtrOssSnake
  18. nektro commented on Jun 17, 2023

    @nektro
    Contributor

    instead of text asm we could perhaps have a std.asm.<arch> ? since llvm already forgoes optimization when using inline asm, by using functions and comptime we could ensure its safety

    and then language-level inline asm would only be text thats generated at comptime by the functions

  19. lerno commented on Jun 19, 2023

    @lerno

    @nektro Do you mean essentially making a mini DSL using compile time functions corresponding to the particular instructions?

  20. eLeCtrOssSnake commented on Jun 19, 2023

    @eLeCtrOssSnake

    instead of text asm we could perhaps have a std.asm.<arch> ? since llvm already forgoes optimization when using inline asm, by using functions and comptime we could ensure its safety

    and then language-level inline asm would only be text thats generated at comptime by the functions

    This immense amount of work to even make these simple intrinsics FOR EVERY ARCH, excluding adding safety. Also, what do you mean by llvm forgoes optimization for inline asm? It does everything in it's power to make the assembly block performant based on inputs, outputs, clobbers. It won't touch your asm code, for the reasons you(programmer) wrote it in the first place.

  21. ethindp commented on Jul 21, 2023

    @ethindp

    Why not just make asm blocks with a zig-like DSL? Reuse as much zig syntax as possible:

    asm volatile {
        mov (rax, ecx);
    }

    IMO the asm syntax shouldn't be annoying or ridiculously complicated. As it currently stands, the current asm syntax definitely doesn't convey intent precisely, nor do any of the proposals that I've seen on this thread -- they all just make everything more confusing to me, and I assume it's confusing to the majority of others who try to write it, let alone read it. Though this isn't a problem unique to Zig.

    @eLeCtrOssSnake You want a "robust" solution for asm syntax; many of us want an easy to read and write one too. The only way you could achieve a robust one is to write your own assembler. It'll take a lot of work, but that's inevitable anyway unless you don't want anything checking the correctness of your code and just "assuming" you're an assembly genius who always knows what they're doing. Sure, LLVM is good at optimizing asm blocks with inputs/outputs/clobber specs, but it sacrifices readability and writability and it doesn't actually check the constraint codes you use other than verifying they're correct.

  22. ethindp commented on Jul 21, 2023

    @ethindp

    The MSVC way is good; I know that D and Pascal do something like it. But it lacks any constraint mechanism. But that could probably be added. Point is, if we're going to stick with ridiculously unreadable inline assembly I might as well just forgo inline asm and just write a bunch of asm in a big .s file for the architecture I'm trying to support; that at least is readable and comprehensible. It doesn't seem too unreasonable to expect a similar guarantee from my programming language.

  23. coffeebe4code commented on Dec 29, 2023

    @coffeebe4code
    Contributor

    Would one even need to specify clobbers if every arch and instruction were supported? Is there a scenario where we wouldn't know which regs were clobbered?

  24. Spitz7279 commented on Jun 13, 2024

    @Spitz7279

    I see a lot of newly proposed syntax to structure the inputs, outputs, and clobbers of an inline assembly. This is all in my opinion unreadable and difficult for a beginner to understand. I believe this could all be implemented reusing existing Zig syntax of anonymous structs, tuples, and enum literals:

    // asm [volatile] (
    //     assembly string,
    //     anonymous struct of ins
    //     anonymous struct of outs
    //     anonymous struct (tuple) of clobbers
    // )
    pub fn syscall3(number: usize, arg1: usize, arg2: usize, arg3: usize) usize {
        return asm volatile (
            "syscall",
            .{
                .rax = .{ number, "number" },
                .rdi = .{ arg1, "arg1" },
                .rsi = .{ arg2, "arg2" },
                .rdx = .{ arg3, "arg3" },
            },
            .{ .rax = usize },
            .{ .rcx, .r11 },
        );
    }

    I apologize for using a simple syscall as an example, but I do not know assembly, and I am afraid I could not write a correct string of more complex assembly. I hope it gets my idea of the syntax across. Giving the inputs types could either be done through the type of the variable passed in or another argument of the tuple. Since this uses entirely valid existing Zig syntax, this could even use a builtin instead of a keyword. I am, however, unsure of using a builtin due to the inability to use the volatile keyword. Volatility could be accomplished through a boolean flag, but I would much prefer the usage of the existing volatile keyword.

  25. ethindp commented on Jun 14, 2024

    @ethindp

    @Spitz7279 I'm not exactly sure how what your proposing is any more readable than a syntax like that which I proposed previously. I mean yes that reuses (some) zig syntax but at this point why not make the syntax just automatically deduce templated arguments or something?

  26. Spitz7279 commented on Jun 14, 2024

    @Spitz7279

    My proposed usage reuses all valid Zig syntax. Outside of an inline assembly block, it would be valid Zig that you could parse on your own. It also retains the comptime known string of assembly, allowing the developer to use @embed. That DSL you proposed looks like valid Zig, but wouldn't really function the same. It would function more like @cImport and @cInclude currently do, since the compiler would need to be told all of the inputs and clobbers (note that your proposed syntax lacks the ability to specify the returned registers and their types.) As far as I am aware, there are possible plans to scrap the current c import model in favor of integration with the build system. I choose to ignore all of the massive burden that it would place to create the DSL and then maintain it as new assembly instructions and registers are added. Although I am unaware of any cases of this, as I do not know assembly much at all, the DSL might not be able to sufficiently deal with the intricacies of a certain platform.

  27. Timmmm commented on Feb 12, 2025

    @Timmmm

    I'm new to this but I hope whatever syntax is used fixes these issues with GCC's:

    • volatile is a terrible name. Not at all clear what it does from the name. Can we have something like asm with_side_effects or asm(side_effects=true) or asm keep or asm ... outputs=foo, bar, side_effects or asm ... outputs=foo, bar, other. Anything but volatile!
    • The order of :inputs :outputs :clobbers :gotos is just way too hard to remember. Did you spot that it's actually :outputs :inputs? Please make it more obvious where the outputs and inputs are (also don't forget about asm goto).

    Good luck!

  28. modified the milestones: upcoming, urgent on Jun 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions