Skip to content

beginnings of (non-LLVM) self-hosted machine code generation and linking #5158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 24, 2020

Conversation

andrewrk
Copy link
Member

This is the beginning of a self-hosted incremental linker (#1535) as well as pure Zig backend.

Here's a demo:

hello.zir

@0 = str("Hello, world!\n")
@1 = primitive(noreturn)
@2 = primitive(usize)
@3 = fntype([], @1, cc=Naked)
@4 = int(0)
@5 = int(1)
@6 = int(231)
@7 = str("len")

@8 = fn(@3, {
  %0 = as(@2, @5) ; SYS_write
  %1 = as(@2, @5) ; STDOUT_FILENO
  %2 = ptrtoint(@0) ; msg ptr
  %3 = fieldptr(@0, @7) ; msg len ptr
  %4 = deref(%3) ; msg len
  %sysoutreg = str("={rax}")
  %rax = str("{rax}")
  %rdi = str("{rdi}")
  %rsi = str("{rsi}")
  %rdx = str("{rdx}")
  %rcx = str("rcx")
  %r11 = str("r11")
  %memory = str("memory")
  %syscall = str("syscall")
  %5 = asm(%syscall, @2,
    volatile=1,
    output=%sysoutreg,
    inputs=[%rax, %rdi, %rsi, %rdx],
    clobbers=[%rcx, %r11, %memory],
    args=[%0, %1, %2, %4])

  %6 = as(@2, @6) ;SYS_exit_group
  %7 = as(@2, @4) ;exit code
  %8 = asm(%syscall, @2,
    volatile=1,
    output=%sysoutreg,
    inputs=[%rax, %rdi],
    clobbers=[%rcx, %r11, %memory],
    args=[%6, %7])

  %9 = unreachable()
})

@9 = str("_start")
@10 = export(@9, @8)

Now run the example program:

[nix-shell:~/dev/zig/build]$ ./ir hello.zir 
@0 = str("Hello, world!\n")
@1 = str("{rax}")
@2 = str("{rdi}")
@3 = str("{rsi}")
@4 = str("{rdx}")
@5 = str("rcx")
@6 = str("r11")
@7 = str("memory")
@8 = primitive(usize)
@9 = int(1)
@10 = as(@8, @9)
@11 = primitive(usize)
@12 = int(1)
@13 = as(@11, @12)
@14 = primitive(usize)
@15 = int(14)
@16 = as(@14, @15)
@17 = str("syscall")
@18 = primitive(usize)
@19 = str("={rax}")
@20 = str("{rax}")
@21 = str("{rdi}")
@22 = str("rcx")
@23 = str("r11")
@24 = str("memory")
@25 = primitive(usize)
@26 = int(231)
@27 = as(@25, @26)
@28 = primitive(usize)
@29 = int(0)
@30 = as(@28, @29)
@31 = str("syscall")
@32 = primitive(usize)
@33 = str("={rax}")
@34 = primitive(noreturn)
@35 = fntype([], @34, cc=Naked)
@36 = fn(@35, {
  %0 = ptrtoint(@0)
  %1 = asm(@17, @18, volatile=1, output=@19, inputs=[@1, @2, @3, @4], clobbers=[@5, @6, @7], args=[@10, @13, %0, @16])
  %2 = asm(@31, @32, volatile=1, output=@33, inputs=[@20, @21], clobbers=[@22, @23, @24], args=[@27, @30])
  %3 = unreachable()
})
@37 = str("_start")
@38 = export(@37, @36)

This example code renders back to ZIR after semantic analysis is complete, and also creates a.out:

[nix-shell:~/dev/zig/build]$ ./a.out 
Hello, world!

[nix-shell:~/dev/zig/build]$ strace ./a.out 
execve("./a.out", ["./a.out"], 0x7ffef9ea81a0 /* 128 vars */) = 0
write(1, "Hello, world!\n", 14Hello, world!
)         = 14
exit_group(0)                           = ?
+++ exited with 0 +++

[nix-shell:~/dev/zig/build]$ objdump -d a.out 

a.out:     file format elf64-x86-64


Disassembly of section .text:

0000000008000000 <_start>:
 8000000:	eb 0e                	jmp    8000010 <_start+0x10>
 8000002:	48                   	rex.W
 8000003:	65 6c                	gs insb (%dx),%es:(%rdi)
 8000005:	6c                   	insb   (%dx),%es:(%rdi)
 8000006:	6f                   	outsl  %ds:(%rsi),(%dx)
 8000007:	2c 20                	sub    $0x20,%al
 8000009:	77 6f                	ja     800007a <_start+0x7a>
 800000b:	72 6c                	jb     8000079 <_start+0x79>
 800000d:	64 21 0a             	and    %ecx,%fs:(%rdx)
 8000010:	b8 01 00 00 00       	mov    $0x1,%eax
 8000015:	bf 01 00 00 00       	mov    $0x1,%edi
 800001a:	48 8d 35 e1 ff ff ff 	lea    -0x1f(%rip),%rsi        # 8000002 <_start+0x2>
 8000021:	ba 0e 00 00 00       	mov    $0xe,%edx
 8000026:	0f 05                	syscall 
 8000028:	b8 e7 00 00 00       	mov    $0xe7,%eax
 800002d:	31 ff                	xor    %edi,%edi
 800002f:	0f 05                	syscall 
 8000031:	cc                   	int3   

Here you can see the "Hello, World!" string is embedded directly into the function. That's not necessarily how it is going to work in the future.

A plan for incremental recompilation

I do have a plan for incremental compilation now (only applicable to debug mode):

The idea is that every top level declaration in zig maps to an ELF symbol. All functions are generated with Position Independent Code. Part of what the cache stores is a mapping from zig top level decl to the decls that depend on it directly.

When a file is detected as changed, we then re-parse the AST of that file looking for AST changes per decl. Now we have a set of decls that is changed. Follow the dependency tree, regenerating decls.

Now we have a set of regenerated decls, and each one maps directly to a symbol in the ELF file. So we only have to go modify symbols in the ELF file that correspond to changed decls.

Next steps

  • Hook this up to tests right away
  • Make it work for PE (Portable Executable) as well
  • Figure out how comptime code & control flow will work with ZIR code.
  • Start reworking the frontend to integrate with the backend
  • Language Server
  • Set up performance and resource usage testing & tracking
  • Iterate on incremental compilation
  • Async I/O / taking advantage of multiple threads
  • Emit DWARF info

const fs = std.fs;
const elf = std.elf;

const executable_mode = 0o755;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const executable_mode = 0o755;
const executable_mode = 0o777;

On common systems with a 022 umask, this will still result in a file created with 755 permissions, but it works appropriately if the system is configured more leniently. (As another data point, C's fopen seems to open files with the 666 mode.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking what GCC and Clang do under strace, they seem to

  1. create the file with 666 permissions,
  2. use stat to determine the actual resulting permissions (modified by the umask)
  3. use umask twice, first to get the current umask and then to reset it (there isn't a side-effect-free way to get the umask that I know of)
  4. use chmod to add the executable bit, manually masking out the umask

As a specific example, running (umask 124 && strace -ff clang test.c) produced

[pid 24421] openat(AT_FDCWD, "a.out", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
[pid 24421] stat("a.out", {st_mode=S_IFREG|0642, st_size=16312, ...}) = 0
[pid 24421] umask(000)                  = 0124
[pid 24421] umask(0124)                 = 000
[pid 24421] chmod("a.out", 0653)        = 0

(among other lines, of course). GCC is similar. Opening with 777 permissions from the start would produce the same result in a much simpler way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I committed your suggestion along with your explanation as a comment.

lib/std/mem.zig Outdated
/// Round an address up to the previous aligned address
/// The alignment must be a power of 2 and greater than 0.
pub fn alignBackwardGeneric(comptime T: type, addr: T, alignment: T) T {
assert(@popCount(T, alignment) == 1);
// 000010000 // example addr
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 000010000 // example addr
// 000010000 // example alignment

I know this isn't actually changed, but while you are here and it appears in context, alignment is the value that is shifted and inverted. (As a side note, negation is equivalent: in two's complement, -x = ~(x - 1).)

Jonathan S writes:

On common systems with a 022 umask, this will still result in a
file created with 755 permissions, but it works appropriately if the
system is configured more leniently. (As another data point, C's fopen
seems to open files with the 666 mode.)
@andrewrk andrewrk merged commit 7634e67 into master Apr 24, 2020
@andrewrk andrewrk deleted the zir-to-elf branch April 24, 2020 19:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants