Zig: Proposal: Builtin type for register-only variables

Created on 9 Apr 2020  路  17Comments  路  Source: ziglang/zig

C code interprets linker symbols as variables, when actually they only have meaning as values -- so, to get sensible behaviour from them when linking, you need to take their address every time. This is ugly.

Zig (and C) requires all local variables take up stack space. In a nakedcc context, this means no local variables are allowed, and globals have to be used instead (as on one memorable occasion). This is annoying.

I propose a new kind of variable: one which is never allocated stack space, but instead resides solely in registers. These variables can only ever be meaningfully passed by value, and interpreted as such, regardless of source (local, global, external, argument). On top of conveniences mentioned above, this could also help optimisation, as long-lived values could avoid being needlessly copied in and out of memory.

Of course, this would only be feasible for architectures with a lot of registers, there would be restrictions on their type, and you couldn't have too many at a time. And I suppose there would be no reason to forbid them being pushed onto the stack for procedure calls, as long as they're still treated as register-only after the call lead-out.

EDIT: Suggested API (thanks @zzyxyzz):

fn test() void {
    var x = @regReserve(i64, "r12", 1); // type, register, init value
    var y: @Reg(i64) = undefined; // equivalent to @regReserve(i64, "any", undefined)
                                  // -- assignment to "any" permits runtime movement
                                  // by the compiler
    // var z = &x; // compile error -- this doesn't make sense
    y.write(2);
    // Other than the next line, everything in this
    // function is possible in callconv(.Naked)
    var z = x.val + y; // values can be accessed, or implicitly cast
    _ = x.free(); // allows r12 to be used again, drops x from
                  // the symbol table, and evaluates to x.val
    var w: @Reg(*i64) = &z; // any type which fits in a register is allowed
}

extern const _sdata: @Reg(*u8); // the _sdata pointer is bitcast to the type in parentheses,
                                // but is not necessarily reserved a register -- just inlined
                                // (`extern var: @Reg` is not permitted)

var sp = @regReserve(*u64, "esp", stack_top); // @Reg's declared with a specific register must
                                              // not clash with any others in a compatible scope
proposal

Most helpful comment

Another use case:

Storing encryption keys in registers, with the compiler guaranteeing that they are not spilled into main memory. Together with #1776 this could go a long way towards writing (reasonably) portable and secure crypto code.

All 17 comments

As I understand, this is all already done by the optimizer. When the optimizer can elide a variable from being allocated on the stack, it does. If it doesn't, then it's just a missed opportunity in the optimizer.

If the main reason for a feature like this is optimization, then I would just handle it in the optimizer. However, if you're trying to get some new kind of functionality, then that would be a different story. You mentioned something about how this would help ease development in a nakedcc context? What did you mean by that?

You mentioned something about how this would help ease development in a nakedcc context? What did you mean by that?

Naked functions can't use a stack. Local variables depend on a stack, so currently naked functions can't have local variables. In the linked video, Andrew is trying to calculate the initial value of a stack pointer, but he can't do that within the _start function (the logical place to do it) because that would require storing the result in a local variable. So, he has to create an entirely new global, taking up more space in the binary, for a calculation that could easily be done at runtime in raw assembly, because there we'd be able to do this calculation without touching the stack. This proposal could allow Zig to do that without dropping to asm.

Admittedly I didn't word the initial proposal very well. I'll go back and fix it.

Is the general idea to introduce syntactic sugar for this very specific use of what could only otherwise be implemented in inline assembly?

Not quite. Inline asm requires specifying exact registers, and each block is entirely self-contained. This would allow the compiler to choose the registers, and enable mixing of these values with other Zig code. Also, this won't tie the program to a specific ISA.

I tend to think of inline asm as a last resort. Yes, it's good that we have it, but it's ugly and it introduces exactly the cognitive overhead and platform dependence that we're writing Zig rather than asm to avoid.

Another way to solve the problem of having local variables in a naked function would be to use assembly. However, instead of just using raw assembly, you could put your assembly into "naked" functions to create a processor-agnostic API that enables low-level access to the stack.

I imagine you could also make an API to store/access registers in a limited processor agnostic way.

I think for this use case, you could find a solution within the existing language semantics that could accomplish the same result. We could determine this for sure if we came up with a concrete example to attempt. See if there's an example we can't implement with these techniques but that the proposed register-only variables would.

@EleanorNB
Having a nakedcc just means there is no stack frame setup for your local variables. However that doesn't mean you cannot manually setup/teardown a stack frame using inline asm. You also are able to call "normal" functions that automatically handle the stack frame from within a nakedcc func, since they clean themselves up.

I think you may need to reevaluate why you are using a nakedcc function and if there are other ways to approach the problem before a niche feature is added for your specific use case.

I don't believe your point about zig making it so you can avoid platform dependence accurate. When dealing with low level specifics are handled is something you unfortunately cant avoid being platform specific (i.e syscalls/stack/registers).

One comment on portability: Allowing a language feature "enforce this into a register" will remove the ability to compile code to stack machines like the JVM or the CLR as they don't have the concept of a register.

I would not impose such a restriction in language level

@marler8997: I'm not exactly sure what you mean. If I understand correctly, you're saying I could write asm and structure it inside functions, and maybe I could create an API to access individual registers, and even have access to a stack if I need it. In that case, though, I might as well just write asm with the help of a snippet engine, because I'd be creating an entirely insular system -- Zig as it stands assumes it can do whatever it likes with registers, so once I call out of asm all of my state will be clobbered. This proposal allows interaction between low-level semantics and regular semantics, which is not possible in current Zig.
As an example, again I use the linked video -- yes, the required calculation could have been done in asm, but that would require all the logic that interacts with those values, and all the logic in between, to also be written within the same block of asm. Possible within the language, but incredibly tedious, and not much better than writing a separate file entirely. We're not making a Turing tarpit, ease of use should be a priority of ours.

@suirad: Yes, you _could_ do that, but in many cases that's unnecessary, and it's incredibly frustrating and tedious to be forced to do that every time you want to do a calculation outside asm. Don't get me wrong, I'm one of the few people who actually enjoys writing asm, I just don't want to hold the semantics of two languages in my head while I'm working. That same principle is a large part of why Zig exists in the first place.
Zig is pitched as a no-strings-attached, strictly-dominating replacement for C (as opposed to something like Rust, which focuses mostly on the mid- to high-level domains), making it the only other real competitor at the _really_ low end -- the kind of work where raw asm is also typically involved. From that perspective, I don't consider this a niche feature at all -- on the contrary, I see it as a killer feature. This way, Zig could come close to being a replacement for asm in some domains as well. Even if we can never fully eliminate asm, we can at least depend on it less.

@MasterQ32: We could just make it a no-op on those architectures, much like async can still be used on blocking functions. This would primarily be for performance and bare-metal work, two things that don't really apply in VMs.

I wonder if this functionality could be provided by a builtin? Something like

var x = @regReserve(i64, "r12");
@regWrite(x, 1);
var y = @regReserve(i64, "any");
@regWrite(y, 2);
var z = @regRead(x) + @regRead(y);

That sure is verbose, but maybe appropriate to the niche-ness of the proposed use-case.

I think regWrite should be a method and regRead should be a property, but otherwise this is perfect.

@EleanorNB
In that case, though, I might as well just write asm with the help of a snippet engine, because I'd be creating an entirely insular system -- Zig as it stands assumes it can do whatever it likes with registers, so once I call out of asm all of my state will be clobbered.

I see. At this point there's alot of details I would have to dig into in order to determine if this is really the best we can do in the existing language. I think either myself or someone needs to take the example from the video and come up with the best solution with the existing language and see how it compares to the proposed feature.

I'd give it a try, but some of the more obscure features of the language aren't very well documented at the moment. I think the only person I'd trust with this is Andrew.

Another use case:

Storing encryption keys in registers, with the compiler guaranteeing that they are not spilled into main memory. Together with #1776 this could go a long way towards writing (reasonably) portable and secure crypto code.

Another use case:

Storing encryption keys in registers, with the compiler guaranteeing that they are not spilled into main memory. Together with #1776 this could go a long way towards writing (reasonably) portable and secure crypto code.

If your process is evicted by the scheduler then all register contents are spilled to kernel memory somewhere. Given the nature of rising abundance of speculative exploits against kernel memory, I don't know how much this use-case is actually strengthened in practice. It would, I think, at least provide some protection against untrusted in-process memory, but my thought is why ship untrusted code in the first place.

If your process is evicted by the scheduler then all register contents are spilled to kernel memory somewhere. Given the nature of rising abundance of speculative exploits against kernel memory, I don't know how much this use-case is actually strengthened in practice. It would, I think, at least provide some protection against untrusted in-process memory, but my thought is why ship untrusted code in the first place.

That's a good point. From a security perspective, modern computers basically consist of nothing but leaks and side-channels :smile: . But crypto libraries still care a lot about being as constant-time and in-register as possible, AFAIK. Probably to stuff as many leaks as possible on a best-effort basis, even though full isolation is impossible.

Now that the concepts are clearer, I want to redo this proposal, so I'm closing for now. Watch this space for the next iteration.

New proposal is up, check it out.

Was this page helpful?
0 / 5 - 0 ratings