diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index a52bcf3f..39f0b82e 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -59,6 +59,13 @@ runs: [ "$(uname -m)" != aarch64 ]; then packages+=(gcc-arm-linux-gnueabihf) fi + # An Arm64 host runs the AArch64 output natively, so mk/arm64.mk + # turns USE_QEMU off and the build resolves the interpreter and libc + # from the running system instead of from a cross sysroot. + if [ "$LINK_MODE" = dynamic ] && [ "$ARCHITECTURE" = arm64 ] && \ + [ "$(uname -m)" != aarch64 ]; then + packages+=(gcc-aarch64-linux-gnu) + fi sudo apt-get update -q -y sudo apt-get install -q -y --no-install-recommends "${packages[@]}" @@ -160,18 +167,21 @@ runs: echo "$sha256 $file" | sha256sum -c - sudo apt-get install -q -y --no-install-recommends "$file" - # An Arm64 runner is asked for so that the Arm output runs through the - # kernel rather than an emulator, and the step above is what lets - # mk/arm.mk see that. Read the decision back while it is fresh: a runner - # the probe stops matching would otherwise turn those jobs into slower - # copies of the x86 ones without saying so. An empty TARGET_EXEC is the - # Makefile saying nothing stands in front of the binary. - - name: Confirm the Arm output runs natively - if: runner.arch == 'ARM64' && inputs.architecture == 'arm' + # An Arm64 runner is asked for so that the Arm and AArch64 output runs + # through the kernel rather than an emulator: mk/arm.mk decides that from + # the step above, mk/arm64.mk from the host architecture alone. Read the + # decision back while it is fresh: a runner the probe stops matching would + # otherwise turn those jobs into slower copies of the x86 ones without + # saying so. An empty TARGET_EXEC is the Makefile saying nothing stands in + # front of the binary. + - name: Confirm the output runs natively + if: runner.arch == 'ARM64' && (inputs.architecture == 'arm' || inputs.architecture == 'arm64') shell: bash + env: + ARCHITECTURE: ${{ inputs.architecture }} run: | set -euo pipefail - runner=$(make -np ARCH=arm 2>/dev/null | \ + runner=$(make -np ARCH="$ARCHITECTURE" 2>/dev/null | \ sed -n 's/^TARGET_EXEC = *//p' | head -1) if [ -n "$runner" ]; then echo "Expected native execution, but the build selected: $runner" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 75a29cfb..81a7c242 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: fail-fast: false matrix: compiler: [gcc, clang] - architecture: [arm, riscv, x64] + architecture: [arm, arm64, riscv, x64] link_mode: [static, dynamic] env: CC: ${{ matrix.compiler }} @@ -80,7 +80,7 @@ jobs: # clang-sanitized one, which puts a single cell of this matrix at # roughly fifteen hours. Restoring the dimension means finding out # why first. - architecture: [arm, riscv, x64] + architecture: [arm, arm64, riscv, x64] link_mode: [static, dynamic] env: ARCH: ${{ matrix.architecture }} @@ -110,7 +110,7 @@ jobs: fail-fast: false matrix: compiler: [gcc, clang] - architecture: [arm, riscv, x64] + architecture: [arm, arm64, riscv, x64] env: CC: ${{ matrix.compiler }} ARCH: ${{ matrix.architecture }} @@ -130,17 +130,21 @@ jobs: - name: Build stage 1 artifact run: ./out/shecc --no-libc -o out/shecc-stage1.elf ./out/out.c - # An Arm64 runner executes the Arm output natively, so this covers the paths - # that the emulator would otherwise stand in for. + # An Arm64 runner executes both the Arm and the AArch64 output natively, so + # this covers the paths that the emulator would otherwise stand in for. For + # AArch64 that is the only place the real loader is exercised: QEMU-user maps + # the image itself, and on an x86-64 host it always presents 4 KiB pages. host-arm: - name: arm/${{ matrix.link_mode }} on Arm64 + name: ${{ matrix.architecture }}/${{ matrix.link_mode }} on Arm64 runs-on: ubuntu-24.04-arm timeout-minutes: 30 strategy: fail-fast: false matrix: + architecture: [arm, arm64] link_mode: [static, dynamic] env: + ARCH: ${{ matrix.architecture }} DYNLINK: ${{ matrix.link_mode == 'dynamic' && '1' || '0' }} steps: - name: Checkout code @@ -148,18 +152,18 @@ jobs: - name: Set up the build environment uses: ./.github/actions/setup-build-env with: - architecture: arm + architecture: ${{ matrix.architecture }} link-mode: ${{ matrix.link_mode }} github-token: ${{ github.token }} - name: Build artifacts - run: make ARCH=arm DYNLINK="$DYNLINK" + run: make ARCH="$ARCH" DYNLINK="$DYNLINK" - name: Unit tests - run: make check ARCH=arm DYNLINK="$DYNLINK" + run: make check ARCH="$ARCH" DYNLINK="$DYNLINK" - name: Upload the test logs if: failure() uses: actions/upload-artifact@v7 with: - name: logs-arm64-host-${{ matrix.link_mode }} + name: logs-arm64-host-${{ matrix.architecture }}-${{ matrix.link_mode }} path: | out/*.log out/tests/*.log diff --git a/Makefile b/Makefile index ed56d7bd..6661e623 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ USE_QEMU ?= 1 OUT ?= out # Every architecture that can be selected as a build target. The first is the # default when ARCH is not given. -ARCHS = arm riscv x64 +ARCHS = arm arm64 riscv x64 ARCH ?= $(firstword $(ARCHS)) SRCDIR := $(shell find src -type d) LIBDIR := $(shell find lib -type d) diff --git a/README.md b/README.md index 4d0786e2..bb7147a4 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ ## Introduction -`shecc` is built from scratch, targeting 32-bit Arm, 32-bit RISC-V, and x86-64, -as a self-compiling compiler for a subset of the C language. +`shecc` is built from scratch, targeting 32-bit Arm, AArch64, 32-bit RISC-V, +and x86-64, as a self-compiling compiler for a subset of the C language. Despite its simplistic nature, it is capable of performing basic optimization strategies as a standalone optimizing compiler. ### Features -* Generate executable Linux ELF binaries for ARMv7-A, RV32IM, and x86-64. +* Generate executable Linux ELF binaries for ARMv7-A, AArch64, RV32IM, and x86-64. * Provide a minimal C standard library for basic I/O on GNU/Linux. * The cross-compiler is written in ANSI C, making it compatible with most platforms. * Include a self-contained C front-end with an integrated machine code generator; no external assembler or linker needed. @@ -19,7 +19,7 @@ Despite its simplistic nature, it is capable of performing basic optimization st * Develop a register allocation system that is compatible with RISC-style architectures. * Implement an architecture-independent, [static single assignment](https://en.wikipedia.org/wiki/Static_single-assignment_form) (SSA)-based middle-end for enhanced optimizations. * Support dynamic linking to allow generated executables to run with glibc. -* Emit both ELF32 (Arm, RISC-V) and ELF64 (x86-64) images; the ELF class follows the target pointer width. +* Emit both ELF32 (Arm, RISC-V) and ELF64 (AArch64, x86-64) images; the ELF class follows the target pointer width. ## Compatibility @@ -41,6 +41,10 @@ syntax: * function-like macros with parameters, `__VA_ARGS__`, stringification (`#`), and token pasting (`##`) The Arm backend targets armv7hf with the Linux ABI, verified on Raspberry Pi 3. +The AArch64 backend follows AAPCS64 and supports static and eager-bound dynamic +linking, verified with QEMU AArch64 on eMag. Its images separate the load +segments by 64 KiB so they load under any of the 4 KiB, 16 KiB and 64 KiB +translation granules AArch64 Linux may be configured with. The RISC-V backend targets RV32IM, verified with QEMU. The x86-64 backend follows the System V AMD64 ABI and runs natively on an x86-64 GNU/Linux host, so no emulator is involved. @@ -86,7 +90,7 @@ the second stage bootstrapping would fail due to `qemu-arm` absence, and the The dynamic linking mode needs an ELF interpreter and the matching glibc for the target. The `x64` target resolves both from the host system, so it needs nothing -beyond an x86-64 GNU/Linux installation. The Arm and RISC-V targets need a +beyond an x86-64 GNU/Linux installation. The Arm, AArch64, and RISC-V targets need a cross-compile GNU toolchain to obtain them. For the Arm architecture, you can install the ARM GNU toolchain using `apt-get`: @@ -98,14 +102,20 @@ Another approach is to manually download and install the toolchain from [ARM Dev Select "x86_64 Linux hosted cross toolchains" - "AArch32 GNU/Linux target with hard float (arm-none-linux-gnueabihf)" to download the toolchain. +For AArch64 dynamic binaries, install the matching toolchain and user emulator: + +```shell +$ sudo apt-get install gcc-aarch64-linux-gnu qemu-user +``` + Since `apt-get` does not provide the necessary RISC-V GNU toolchain, it must be downloaded manually if you want to run a dynamically linked `shecc` targeting the RISC-V architecture. For instance, you can download and extract the `riscv32-glibc-ubuntu-22.04-gcc.tar.xz` package from the [riscv-gnu-gcc](https://github.com/riscv-collab/riscv-gnu-toolchain) repository. ## Build and Verify -Configure which backend you want. `shecc` supports the ARMv7-A, RV32IM, and -x86-64 backends, with Arm as the default: +Configure which backend you want. `shecc` supports the ARMv7-A, AArch64, +RV32IM, and x86-64 backends, with Arm as the default: ```shell $ make config ARCH=arm # Target machine code switch to arm @@ -113,6 +123,9 @@ $ make config ARCH=arm $ make config ARCH=riscv # Target machine code switch to riscv +$ make config ARCH=arm64 +# Target machine code switch to arm64 + $ make config ARCH=x64 # Target machine code switch to x64 ``` @@ -145,7 +158,10 @@ $ make DYNLINK=1 SHECC out/shecc-stage2.elf $ file out/shecc-stage2.elf +# ARCH=arm: out/shecc-stage2.elf: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, not stripped +# ARCH=arm64: +out/shecc-stage2.elf: ELF 64-bit LSB executable, ARM aarch64, dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, no section header ``` For development builds with memory safety checks: @@ -171,20 +187,26 @@ Compiler options: Example 1: static linking mode ```shell $ out/shecc -o fib tests/fib.c -$ chmod +x fib $ qemu-arm fib ``` +The compiler marks its own output executable, so neither example needs a +`chmod +x` in front of the run. Earlier revisions did: the mode used to be +whatever the libc that opened the file chose, which was 0666 for a +glibc-linked build. + An `x64` build produces a native binary, so `./fib` runs it directly with no emulator in front. Example 2: dynamic linking mode -Notice that `/usr/arm-linux-gnueabihf` is the ELF interpreter prefix. Since the path may be different if you manually install the ARM/RISC-V GNU toolchain instead of using `apt-get`, you should set the prefix to the actual path. +For AArch64, `/usr/aarch64-linux-gnu` is a typical ELF interpreter prefix. +The path may differ if you manually install a GNU toolchain, so set it to the +actual sysroot. ```shell $ out/shecc --dynlink -o fib tests/fib.c -$ chmod +x fib -$ qemu-arm -L /usr/arm-linux-gnueabihf fib +$ qemu-arm -L /usr/arm-linux-gnueabihf fib # ARCH=arm +$ qemu-aarch64 -L /usr/aarch64-linux-gnu fib # ARCH=arm64 ``` ### Unit Tests diff --git a/lib/c.c b/lib/c.c index 37081966..6b03909d 100644 --- a/lib/c.c +++ b/lib/c.c @@ -241,13 +241,11 @@ void __str_base10(char *pb, int val) int q, r, t; int i = INT_BUF_LEN - 1; - /* On a 32-bit target, negating INT_MIN overflows and the digit loop below - * cannot make progress, so the value is spelled out directly. On LP64 the - * negation happens in a 64-bit register and the normal path is exact. This - * is an ordinary constant expression rather than a preprocessor conditional - * so that shecc can compile this file for either target. + /* val is an int on every target: negating INT_MIN overflows even when + * pointers and registers are 64-bit. Spell it directly so the digit loop + * never walks its stack buffer backwards indefinitely. */ - if (__ptr_width == 4 && val == -2147483648) { + if (val == -2147483648) { strncpy(pb + INT_BUF_LEN - 11, "-2147483648", 11); return; } @@ -662,14 +660,14 @@ FILE *fopen(char *filename, char *mode) if (!strcmp(mode, "wb")) perm = 0x1fd; -#if defined(__riscv) +#if defined(__riscv) || defined(__aarch64__) /* FIXME: mode not work currently in RISC-V */ fd = __syscall(__syscall_openat, -100, filename, 577, perm); #else fd = __syscall(__syscall_open, filename, 577, perm); #endif } else if (!strcmp(mode, "r") || !strcmp(mode, "rb")) { -#if defined(__riscv) +#if defined(__riscv) || defined(__aarch64__) fd = __syscall(__syscall_openat, -100, filename, 0, 0); #else fd = __syscall(__syscall_open, filename, 0, 0); @@ -692,6 +690,16 @@ int fclose(FILE *stream) return 0; } +int chmod(char *filename, int mode) +{ +#if defined(__riscv) || defined(__aarch64__) + /* sys_fchmodat takes (dirfd, filename, mode); AT_FDCWD is -100. */ + return __syscall(__syscall_fchmodat, -100, filename, mode); +#else + return __syscall(__syscall_chmod, filename, mode); +#endif +} + /* Read a byte from file descriptor. So the return value is either in the range * of 0 to 127 for the character, or -1 on the end of file. */ @@ -738,32 +746,28 @@ int fputc(int c, FILE *stream) int fseek(FILE *stream, int offset, int whence) { int result; -#if defined(__arm__) - result = __syscall(__syscall_lseek, stream, offset, whence); -#elif defined(__riscv) - /* No need to offset */ + + /* RV32 has only _llseek, which splits the offset and returns the result + * through a pointer. Every other target takes (fd, offset, whence) + * directly. lib/c.h rejects an architecture that is neither. + */ +#if defined(__riscv) result = __syscall(__syscall_lseek, stream, 0, offset, NULL, whence); -#elif defined(__x86_64__) - /* x86-64 lseek(2) takes (fd, offset, whence) directly. */ - result = __syscall(__syscall_lseek, stream, offset, whence); #else -#error "Unsupported fseek support for current platform" + result = __syscall(__syscall_lseek, stream, offset, whence); #endif return result == -1; } int ftell(FILE *stream) { -#if defined(__arm__) - return __syscall(__syscall_lseek, stream, 0, SEEK_CUR); -#elif defined(__riscv) + /* See fseek(): only RV32 needs the split-offset _llseek form. */ +#if defined(__riscv) int result; __syscall(__syscall_lseek, stream, 0, 0, &result, SEEK_CUR); return result; -#elif defined(__x86_64__) - return __syscall(__syscall_lseek, stream, 0, SEEK_CUR); #else -#error "Unsupported ftell support for current platform" + return __syscall(__syscall_lseek, stream, 0, SEEK_CUR); #endif } diff --git a/lib/c.h b/lib/c.h index d9be4b15..d097edcf 100644 --- a/lib/c.h +++ b/lib/c.h @@ -21,19 +21,32 @@ #define SEEK_CUR 1 #define SEEK_END 2 -#if defined(__arm__) +/* Pointer width and syscall table are independent axes: RV32 and AArch64 share + * the asm-generic table but not the width, while Arm32 and x86-64 each carry a + * legacy table of their own. Selecting on each separately keeps one copy of the + * numbers. + */ +#if defined(__arm__) || defined(__riscv) #define __SIZEOF_POINTER__ 4 +#elif defined(__aarch64__) || defined(__x86_64__) +#define __SIZEOF_POINTER__ 8 +#else +#error "Unsupported architecture" +#endif + +#if defined(__arm__) #define __syscall_exit 1 #define __syscall_read 3 #define __syscall_write 4 #define __syscall_close 6 #define __syscall_open 5 #define __syscall_lseek 19 +#define __syscall_chmod 15 #define __syscall_mmap2 192 #define __syscall_munmap 91 -#elif defined(__riscv) -#define __SIZEOF_POINTER__ 4 +/* RV32 and AArch64 both use the asm-generic table. */ +#elif defined(__riscv) || defined(__aarch64__) #define __syscall_exit 93 #define __syscall_read 63 #define __syscall_write 64 @@ -41,11 +54,15 @@ #define __syscall_open 1024 #define __syscall_openat 56 #define __syscall_lseek 62 + +/* That table has no chmod at all -- 90 is capget there -- so path-based mode + * changes go through fchmodat(2). + */ +#define __syscall_fchmodat 53 #define __syscall_mmap2 222 #define __syscall_munmap 215 #elif defined(__x86_64__) -#define __SIZEOF_POINTER__ 8 #define __syscall_exit 60 #define __syscall_read 0 #define __syscall_write 1 @@ -53,6 +70,7 @@ #define __syscall_open 2 #define __syscall_openat 257 #define __syscall_lseek 8 +#define __syscall_chmod 90 #define __syscall_mmap 9 #define __syscall_munmap 11 @@ -61,7 +79,7 @@ */ #define __syscall_mmap2 9 -#else /* Only Arm32, RV32, and x86-64 are supported */ +#else #error "Unsupported architecture" #endif @@ -98,6 +116,7 @@ typedef int FILE; FILE *fopen(char *filename, char *mode); int fclose(FILE *stream); +int chmod(char *filename, int mode); int fgetc(FILE *stream); char *fgets(char *str, int n, FILE *stream); int fputc(int c, FILE *stream); diff --git a/mk/arm64.mk b/mk/arm64.mk new file mode 100644 index 00000000..c6b402b2 --- /dev/null +++ b/mk/arm64.mk @@ -0,0 +1,42 @@ +# AArch64 Linux / AAPCS64 target. +ARCH_RUNNER = qemu-aarch64 +ARCH_DEFS = \ + "/* target: AArch64 */\n$\ + \#pragma once\n$\ + \#define ARCH_PREDEFINED \"__aarch64__\"\n$\ + \#define ELF_MACHINE 0xb7 /* EM_AARCH64 */\n$\ + \#define ELF_FLAGS 0\n$\ + \#define PTR_SIZE 8\n$\ + \#define MAX_ARGS_IN_REG 8\n$\ + /* Eight register arguments leave no outgoing stack area at the default\n$\ + * limit of eight, so a ninth argument would land on the caller's own\n$\ + * locals. */\n$\ + \#define MAX_PARAMS 16\n$\ + /* AArch64 Linux uses a 4K, 16K or 64K translation granule. Separating the\n$\ + * load segments by the largest keeps the image loadable on all three; a 4K\n$\ + * gap leaves both inside one 64K page, where the writable mapping replaces\n$\ + * the executable one. */\n$\ + \#define PAGESIZE 65536\n$\ + \#define REG_CNT 11\n$\ + \#define CALLEE_SAVED_REGS 3\n$\ + \#define DYN_LINKER \"/lib/ld-linux-aarch64.so.1\"\n$\ + \#define LIBC_SO \"libc.so.6\"\n$\ + \#define PLT_FIXUP_SIZE 0\n$\ + \#define PLT_ENT_SIZE 16\n$\ + \#define RESERVED_GOT_NUM 3\n$\ + \#define R_ARCH_JUMP_SLOT 1026 /* R_AARCH64_JUMP_SLOT */\n$\ + \#define DYN_BIND_NOW 1\n$\ + " + +# An Arm64 Linux host runs this target's output itself, so nothing has to stand +# in for it. The Arm target needs fastfetch to tell a board that can run its +# 32-bit output from one that cannot; here the host answers on its own. The +# check is for the kernel as well as the architecture, since what comes out is +# an AArch64 Linux ELF and no other system will execute it. With the emulator +# out of the way the dynamic build resolves its interpreter and libc from the +# running system rather than from a sysroot. +ifeq ($(shell uname -s -m),Linux aarch64) + USE_QEMU = 0 +endif + +TOOLCHAIN_CANDIDATES := aarch64-linux-gnu- aarch64-none-linux-gnu- diff --git a/src/arm64-codegen.c b/src/arm64-codegen.c new file mode 100644 index 00000000..e1589a54 --- /dev/null +++ b/src/arm64-codegen.c @@ -0,0 +1,731 @@ +/* + * AArch64 Linux code generator. The allocator's virtual registers map to + * x0..x7, x20..x22; x16/x17 are reserved scratch and x19 is the global base. + */ +#include "defs.h" +#include "globals.c" + +#define A64_SP 31 +#define A64_ZR 31 + +/* IP0, the linker's own scratch register: free for a code generator to use + * between instructions, and never allocated to a value. + */ +#define A64_IP0 16 +/* Base of the synthetic global frame, held for the life of the program. */ +#define A64_GP 19 + +/* AAPCS64 keeps SP 16-byte aligned, and the prologue saves five registers in + * three pairs. Every frame calculation derives from these two. + */ +#define A64_STACK_ALIGN 16 +#define A64_SAVE_BYTES 48 + +int a64_reg(int r) +{ + return r < 8 ? r : r + 12; +} +void emit(int insn) +{ + elf_write_int(elf_code, insn); +} +void a64_mov(int d, int n) +{ + emit(0xaa0003e0 | (n << 16) | d); +} + +/* SP is not a general register in logical instructions: ORR would read ZR. ADD + * #0 is the architectural move spelling when either operand is SP. + */ +void a64_mov_sp(int d) +{ + emit(0x910003e0 | (A64_SP << 5) | d); +} +void a64_mov_imm(int d, int v) +{ + /* Integer constants participate in pointer arithmetic throughout the + * compiler. A negative C int must therefore become a sign-extended + * X-register value, not 0x00000000ffffffff-style zero extension. MOVN + * supplies the upper one bits while MOVK fills the second halfword. + */ + if (v < 0) + emit(0x92800000 | (((~v) & 0xffff) << 5) | d); + else + emit(0xd2800000 | ((v & 0xffff) << 5) | d); + emit(0xf2800000 | (((v >> 16) & 0xffff) << 5) | d | (1 << 21)); +} +void a64_add(int d, int n, int m) +{ + int op = (d == A64_SP || n == A64_SP) ? 0x8b206000 : 0x8b000000; + emit(op | (m << 16) | (n << 5) | d); +} +void a64_sub(int d, int n, int m) +{ + int op = (d == A64_SP || n == A64_SP) ? 0xcb206000 : 0xcb000000; + emit(op | (m << 16) | (n << 5) | d); +} +void a64_sxtw(int d, int n) +{ + emit(0x93407c00 | (n << 5) | d); +} + +/* Xd = Xn +/- sign_extend(Wm). The extended-register forms fold the widening of + * an int index into the address arithmetic that consumes it, so pointer + * arithmetic costs the same one instruction as any other add. + */ +void a64_add_sxtw(int d, int n, int m) +{ + emit(0x8b20c000 | (m << 16) | (n << 5) | d); +} +void a64_sub_sxtw(int d, int n, int m) +{ + emit(0xcb20c000 | (m << 16) | (n << 5) | d); +} + +/* Sign-extend the low @size bytes of Xn into Xd. Always one instruction, so + * update_elf_offset()'s default estimate stays correct. + */ +void a64_extend(int d, int n, int size) +{ + if (size == 1) + emit(0x93401c00 | (n << 5) | d); + else if (size == 2) + emit(0x93403c00 | (n << 5) | d); + else if (size == 4) + a64_sxtw(d, n); + else + a64_mov(d, n); +} + +/* AArch64's ordinary LDR/STR immediate is scaled by the access width. The + * compiler deliberately supports packed C layouts, so structure members are + * often not naturally aligned (func_t.bbs, for example). Such offsets must use + * the byte-addressed LDUR/STUR form; rounding them down corrupts adjacent + * fields during self-hosting. + */ +int a64_mem_count(int size, int ofs) +{ + if (ofs >= 0 && ofs <= 4095 * size && !(ofs % size)) + return 1; + if (ofs >= -256 && ofs <= 255) + return 1; + return 4; /* MOVZ/MOVK + ADD + [base] access */ +} + +/* Base opcode of the unscaled load/store of @size bytes. Size lives in bits + * 31:30 and the operation in 23:22, where a load picks the sign-extending form + * for every width narrower than a doubleword -- values sit sign-extended in + * their X register, so a narrow load must widen the same way whichever + * addressing form carries it. The scaled form is this plus bit 24, which is why + * one table serves both and they can no longer disagree. + */ +int a64_mem_op(int load, int size) +{ + int sf; + if (size == 8) + sf = 3; + else if (size == 4) + sf = 2; + else if (size == 2) + sf = 1; + else if (size == 1) + sf = 0; + else { + fatal("unsupported arm64 access width"); + return 0; + } + if (!load) + return 0x38000000 | (sf << 30); + return 0x38000000 | (sf << 30) | ((size == 8 ? 1 : 2) << 22); +} +void a64_mem(int load, int size, int rt, int rn, int ofs) +{ + int op = a64_mem_op(load, size); + if (ofs >= -256 && ofs <= 255 && (ofs < 0 || ofs % size)) { + emit(op | ((ofs & 0x1ff) << 12) | (rn << 5) | rt); /* LDUR/STUR */ + return; + } + if (ofs < 0 || ofs > 4095 * size || ofs % size) { + a64_mov_imm(A64_IP0, ofs); + a64_add(A64_IP0, rn, A64_IP0); + rn = A64_IP0; + ofs = 0; + } + emit(op | (1 << 24) | ((ofs / size) << 10) | (rn << 5) | rt); +} + +/* Masking an out-of-range displacement silently branches somewhere else. The + * generated image is small enough that these never fire today, so say so rather + * than let a larger input fail as a wild jump. + */ +void a64_check_disp(int disp, int bits, char *what) +{ + int lim = 1 << (bits - 1); + if (disp < -lim || disp >= lim) + fatal(what); +} + +/* Branch to @target when @rt is non-zero. CBNZ carries the same 19-bit + * displacement a B.cond would, so it replaces a compare-and-branch pair + * outright. @wide picks the X form, which a pointer needs: testing only Wn + * reads an address whose low word happens to be zero as null. + */ +void a64_cbnz(bool wide, int rt, int target) +{ + int disp = (target - elf_code->size) / 4; + a64_check_disp(disp, 19, "arm64 conditional branch out of range"); + emit((wide ? 0xb5000000 : 0x35000000) | ((disp & 0x7ffff) << 5) | rt); +} +void a64_b(int target) +{ + int d = (target - elf_code->size) / 4; + a64_check_disp(d, 26, "arm64 branch out of range"); + emit(0x14000000 | (d & 0x3ffffff)); +} +void a64_bl_addr(int target) +{ + int d = (target - (elf_code_start + elf_code->size)) / 4; + a64_check_disp(d, 26, "arm64 call out of range"); + emit(0x94000000 | (d & 0x3ffffff)); +} +int a64_adrp_insn(int d, int pc, int target) +{ + int pages = (target >> 12) - (pc >> 12); + a64_check_disp(pages, 21, "arm64 ADRP target out of range"); + return 0x90000000 | ((pages & 3) << 29) | (((pages >> 2) & 0x7ffff) << 5) | + d; +} + +/* Which operand of an address expression is the int index that must widen: 0 + * for neither, 1 for src0, 2 for src1. Exactly one source being an address is + * what makes the other an index, so the two source flags decide alone. + */ +int a64_ptr_index(ph2_ir_t *p) +{ + if (p->src0_is_pointer == p->src1_is_pointer) + return 0; + return p->src0_is_pointer ? 2 : 1; +} + +/* A comparison is as wide as the values it compares. An address has to be + * compared whole, an array included, since what reaches the instruction is its + * decayed base. The source flags say so and is_pointer does not: it counts + * pointer-like operands alone, for the sake of the 32-bit targets that read it. + */ +bool a64_cmp_wide(ph2_ir_t *p) +{ + return p->src0_is_pointer || p->src1_is_pointer; +} + +int a64_cond(opcode_t op) +{ + switch (op) { + case OP_eq: + return 0; + case OP_neq: + return 1; + case OP_geq: + return 10; + case OP_lt: + return 11; + case OP_gt: + return 12; + default: + return 13; + } +} + +/* All estimates below exactly match emit_ph2_ir(). Fixed-width AArch64 makes + * this deliberately simpler and more reliable than a relocation pass. + */ +void update_elf_offset(ph2_ir_t *ir) +{ + int n = 1; + switch (ir->op) { + case OP_allocat: + n = 0; + break; + case OP_assign: + n = ir->dest == ir->src0 ? 0 : 1; + break; + case OP_load_constant: + case OP_load_data_address: + case OP_load_rodata_address: + n = 2; + break; + case OP_address_of: + case OP_global_address_of: + n = 3; + break; + case OP_load: + case OP_global_load: + n = a64_mem_count(ir->size_bytes, ir->src0); + break; + case OP_store: + case OP_global_store: + n = a64_mem_count(ir->size_bytes, ir->src1); + break; + case OP_address_of_func: + n = 3; + break; + case OP_return: + n = (ir->src0 >= 0 && a64_reg(ir->src0) != 0) ? 8 : 7; + break; + case OP_branch: + n = 2; + break; + case OP_mod: + n = 2; + break; + case OP_eq: + case OP_neq: + case OP_gt: + case OP_lt: + case OP_geq: + case OP_leq: + case OP_log_not: + n = 2; + break; + default: + break; + } + elf_offset += n * 4; +} + +void cfg_flatten(void) +{ + func_t *f; + + /* Entry sequence lengths, which the block offsets below start after. + * Static: 12 instructions of setup, then the 9-instruction __syscall + * helper, so 21 in all. Dynamic: 23, having no __syscall helper but saving + * the original stack pointer for __libc_start_main's stack_end argument, + * spilling argc/argv, and calling memset to clear the frame. + */ + f = find_func("__syscall"); + if (f && f->bbs) { + if (dynlink) + f->bbs->elf_offset = 0; + else + f->bbs->elf_offset = 12 * 4; + } + if (dynlink) + elf_offset = 23 * 4; + else + elf_offset = 21 * 4; + GLOBAL_FUNC->bbs->elf_offset = elf_offset; + for (ph2_ir_t *p = GLOBAL_FUNC->bbs->ph2_ir_list.head; p; p = p->next) + update_elf_offset(p); + + /* code_generate() emits the call to main only when there is one, so a + * translation unit without main must not be charged for it either. + */ + if (MAIN_BB) { + int global_frame = ALIGN_UP(GLOBAL_FUNC->stack_size, A64_STACK_ALIGN); + if (dynlink) + elf_offset += (10 + a64_mem_count(8, global_frame) + + a64_mem_count(8, global_frame + 8)) * + 4; + else + elf_offset += 6 * 4; + } + for (f = FUNC_LIST.head; f; f = f->next) { + if (!f->bbs) + continue; + ph2_ir_t *d = add_ph2_ir(OP_define); + d->src0 = f->stack_size; + d->func_name = intern_string(f->return_def.var_name); + + /* Where the caller's stack arguments sit, seen from the callee's own + * SP. This must round the frame exactly as the prologue does: rounding + * to MIN_ALIGNMENT instead left it eight bytes short whenever + * stack_size % 16 == 8, and every stack-passed argument was then read + * one slot low. + */ + int stack_top_ofs = + ALIGN_UP(f->stack_size, A64_STACK_ALIGN) + A64_SAVE_BYTES; + for (basic_block_t *b = f->bbs; b; b = b->rpo_next) { + b->elf_offset = elf_offset; + + /* The entry block's offset deliberately names the prologue so calls + * enter at a valid function entry. Later block labels must however + * account for that prologue before their first IR word. + */ + if (b == f->bbs) { + elf_offset += 7 * 4; + if (dynlink && !strcmp(f->return_def.var_name, "main")) + elf_offset += 3 * 4; + } + for (ph2_ir_t *p = b->ph2_ir_list.head; p; p = p->next) { + if (p->ofs_based_on_stack_top) { + if (p->op == OP_load || p->op == OP_address_of) + p->src0 += stack_top_ofs; + else if (p->op == OP_store) + p->src1 += stack_top_ofs; + } + ph2_ir_t *q = add_existed_ph2_ir(p); + if (q->op == OP_return) + q->src1 = f->stack_size; + update_elf_offset(q); + } + } + } +} + +void emit_ph2_ir(ph2_ir_t *p) +{ + int d = a64_reg(p->dest), n = a64_reg(p->src0), m = a64_reg(p->src1); + switch (p->op) { + case OP_define: { + bool reload_global_base = dynlink && !strcmp(p->func_name, "main"); + emit(0xa9bf7bfd); /* stp x29, x30, [sp, #-16]! */ + emit(0x910003fd); /* mov x29, sp */ + emit(0xa9bf57f4); /* stp x20, x21, [sp, #-16]! */ + /* x19 holds the synthetic global-frame base, but AAPCS64 makes it + * callee-saved and glibc calls into this code at main. Saving it + * alongside x22 costs nothing: the slot was half empty anyway. + */ + emit(0xa9bf4ff6); /* stp x22, x19, [sp, #-16]! */ + a64_mov_imm(A64_IP0, ALIGN_UP(p->src0, A64_STACK_ALIGN)); + a64_sub(A64_SP, A64_SP, A64_IP0); + + /* Reload x19 rather than trust what called us: glibc enters at main, + * and AAPCS64 lets everything in between clobber a callee-saved + * register it has saved. The leading 1 is a64_mem()'s load selector, so + * this reads the base back from the word parked at elf_data_start. The + * frame itself is allocated once in code_generate(), which is also + * where the question of clearing it is settled. + */ + if (reload_global_base) { + a64_mov_imm(A64_IP0, elf_data_start); + a64_mem(1, 8, A64_GP, A64_IP0, 0); + } + return; + } + case OP_load_constant: + a64_mov_imm(d, p->src0); + return; + case OP_assign: + if (d != n) + a64_mov(d, n); + return; + case OP_address_of: + a64_mov_imm(A64_IP0, p->src0); + a64_add(d, A64_SP, A64_IP0); + return; + case OP_global_address_of: + a64_mov_imm(A64_IP0, p->src0); + a64_add(d, A64_GP, A64_IP0); + return; + case OP_load: + a64_mem(1, p->size_bytes, d, A64_SP, p->src0); + return; + case OP_global_load: + a64_mem(1, p->size_bytes, d, A64_GP, p->src0); + return; + case OP_store: + a64_mem(0, p->size_bytes, n, A64_SP, p->src1); + return; + case OP_global_store: + a64_mem(0, p->size_bytes, n, A64_GP, p->src1); + return; + case OP_read: + a64_mem(1, p->src1, d, n, 0); + return; + case OP_write: + a64_mem(0, p->dest, m, n, 0); + return; + case OP_add: + /* Index expressions are int-valued, so the index has to widen before it + * joins a 64-bit address; otherwise -1 becomes +4294967295 on LP64. + * Addition is commutative, so either operand may be the index. + * + * The fall-through stays the X form for both cases a64_ptr_index() + * reports as 0: two addresses, which is pointer minus pointer and + * genuinely 64-bit, and two ints, whose upper half no reader looks at. + * See the width note at the comparisons below. + */ + if (a64_ptr_index(p) == 2) + a64_add_sxtw(d, n, m); + else if (a64_ptr_index(p) == 1) + a64_add_sxtw(d, m, n); + else + a64_add(d, n, m); + return; + case OP_sub: + /* Only pointer-minus-int widens: int-minus-pointer is not an address + * expression, and pointer-minus-pointer is already 64-bit on both + * sides. + */ + if (a64_ptr_index(p) == 2) + a64_sub_sxtw(d, n, m); + else + a64_sub(d, n, m); + return; + case OP_mul: + emit(0x1b007c00 | (m << 16) | (n << 5) | d); + return; + case OP_div: + emit(0x1ac00c00 | (m << 16) | (n << 5) | d); + return; + case OP_mod: + /* d = n % m. Do not put the quotient in d: register coalescing may make + * d alias n, losing the minuend before MSUB reads it. + */ + emit(0x1ac00c00 | (m << 16) | (n << 5) | A64_IP0); + emit(0x1b008000 | (m << 16) | (n << 10) | (A64_IP0 << 5) | d); + return; + case OP_lshift: + emit(0x1ac02000 | (m << 16) | (n << 5) | d); + return; + case OP_rshift: + emit(0x1ac02800 | (m << 16) | (n << 5) | d); + return; + case OP_bit_and: + emit(0x0a000000 | (m << 16) | (n << 5) | d); + return; + case OP_bit_or: + emit(0x2a000000 | (m << 16) | (n << 5) | d); + return; + case OP_bit_xor: + emit(0x4a000000 | (m << 16) | (n << 5) | d); + return; + case OP_negate: + emit(0x4b0003e0 | (n << 16) | d); + return; + case OP_bit_not: + emit(0x2a2003e0 | (n << 16) | d); + return; + case OP_eq: + case OP_neq: + case OP_gt: + case OP_lt: + case OP_geq: + case OP_leq: + emit((a64_cmp_wide(p) ? 0xeb00001f : 0x6b00001f) | (m << 16) | + (n << 5)); + emit((a64_cmp_wide(p) ? 0x9a9f07e0 : 0x1a9f07e0) | + ((a64_cond(p->op) ^ 1) << 12) | d); + return; + + /* Width follows the operand, exactly as the comparisons above do. A pointer + * must be tested whole, or one whose low word happens to be zero reads as + * null. An int must not be: multiply, divide, shift and the bitwise + * operations all use the W forms, which leave the upper half zeroed rather + * than sign-extended, so only the low word is the value. + */ + case OP_log_not: + emit((p->src0_is_pointer ? 0xf100001f : 0x7100001f) | (n << 5)); + emit(0x1a9f07e0 | (1 << 12) | d); + return; + + /* OP_trunc's src1 is the target width; OP_sign_ext's packs the source width + * in its upper half (see promote_unchecked()). Decoding both the same way + * made every promotion a plain move. + */ + case OP_trunc: + a64_extend(d, n, p->src1); + return; + case OP_sign_ext: { + int src_size = (p->src1 >> 16) & 0xffff, dst_size = p->src1 & 0xffff; + + /* Widening to a pointer: the register already holds a full address, so + * extending it from 32 bits would discard the upper half. + */ + if (dst_size == PTR_SIZE) + a64_mov(d, n); + else + a64_extend(d, n, src_size); + return; + } + case OP_cast: + a64_mov(d, n); + return; + case OP_branch: + a64_cbnz(p->src0_is_pointer, n, p->then_bb->elf_offset); + a64_b(p->else_bb->elf_offset); + return; + case OP_jump: + a64_b(p->next_bb->elf_offset); + return; + case OP_call: { + func_t *f = find_func(p->func_name); + if (!f) + fatal("arm64 call to unknown function"); + if (!f->bbs) { + if (!dynlink) + fatal("arm64 external call requires --dynlink"); + a64_bl_addr(dynamic_sections.elf_plt_start + f->plt_offset); + } else + emit(0x94000000 | + (((f->bbs->elf_offset - elf_code->size) / 4) & 0x3ffffff)); + return; + } + case OP_load_data_address: + a64_mov_imm(d, p->src0 + elf_data_start); + return; + case OP_load_rodata_address: + a64_mov_imm(d, p->src0 + elf_rodata_start); + return; + case OP_address_of_func: { + func_t *f = find_func(p->func_name); + int target; + if (!f) + fatal("arm64 address of unknown function"); + if (!f->bbs) { + if (!dynlink) + fatal("arm64 external function address requires --dynlink"); + + /* A PLT entry is a stable callable address. With eager binding, its + * GOT slot already holds the resolved external target. + */ + target = dynamic_sections.elf_plt_start + f->plt_offset; + } else + target = elf_code_start + f->bbs->elf_offset; + a64_mov_imm(A64_IP0, target); + a64_mem(0, 8, A64_IP0, n, 0); + return; + } + case OP_load_func: + a64_mov(A64_IP0, n); + return; + case OP_indirect: + emit(0xd63f0200); + return; + case OP_return: + if (p->src0 >= 0 && n != 0) + a64_mov(0, n); + a64_mov_imm(A64_IP0, ALIGN_UP(p->src1, A64_STACK_ALIGN)); + a64_add(A64_SP, A64_SP, A64_IP0); + emit(0xa8c14ff6); /* ldp x22, x19, [sp], #16 */ + emit(0xa8c157f4); /* ldp x20, x21, [sp], #16 */ + emit(0xa8c17bfd); /* ldp x29, x30, [sp], #16 */ + emit(0xd65f03c0); /* ret */ + return; + default: + fatal("unknown arm64 opcode"); + } +} + +/* Each PLT entry is ADRP/ADD to form the GOT slot address, then an indirect + * branch through it. ADD carries the byte offset rather than folding it into a + * scaled LDR, because .got follows the byte-aligned interpreter string and so + * is not guaranteed to be eight-byte aligned. + */ +void plt_generate(void) +{ + int entries = dynamic_sections.plt_size / PLT_ENT_SIZE; + for (int i = 0; i < entries; i++) { + int ent = dynamic_sections.elf_plt_start + i * PLT_ENT_SIZE; + int got = + dynamic_sections.elf_got_start + PTR_SIZE * (RESERVED_GOT_NUM + i); + elf_write_int(dynamic_sections.elf_plt, + a64_adrp_insn(A64_IP0, ent, got)); + elf_write_int(dynamic_sections.elf_plt, + 0x91000210 | ((got & 0xfff) << 10)); /* add x16, x16, # */ + elf_write_int(dynamic_sections.elf_plt, + 0xf9400211); /* ldr x17, [x16] */ + elf_write_int(dynamic_sections.elf_plt, 0xd61f0220); /* br x17 */ + } +} + +void code_generate(void) +{ + int global_frame = ALIGN_UP(GLOBAL_FUNC->stack_size, A64_STACK_ALIGN); + + /* At Linux process entry argc is at [sp] and argv begins at sp + 8; they + * are loaded into x20 and x21. Those two are allocatable by the global + * initialiser, so the values are parked in x23/x24 -- callee-saved, and + * outside this backend's allocation set -- to survive it. + */ + if (dynlink) + a64_mov_sp(25); + a64_mem(1, 8, 20, A64_SP, 0); + emit(0x910023f5); + a64_mov(23, 20); + a64_mov(24, 21); + if (dynlink) { + a64_mov_imm(A64_IP0, A64_STACK_ALIGN); + a64_sub(A64_SP, A64_SP, A64_IP0); + a64_mem(0, 8, 23, A64_SP, 0); + a64_mem(0, 8, 24, A64_SP, 8); + } + + /* The synthetic global frame is carved out of the runtime stack, and x19 + * keeps its base; only that base is parked in the data segment, where a + * prologue entered from glibc can read it back. + * + * Clearing it is the dynamic build's job alone. A static image is the first + * thing to run, so the stack below the entry SP is untouched anonymous + * memory and already reads as zero. A dynamic one has had the loader and + * glibc's startup run over that same memory first, so a global with no + * initializer would otherwise begin life holding their leftovers. + */ + a64_mov_imm(A64_IP0, global_frame); + a64_sub(A64_SP, A64_SP, A64_IP0); + a64_mov_sp(A64_GP); + a64_mov_imm(A64_IP0, elf_data_start); + a64_mem(0, 8, A64_GP, A64_IP0, 0); + if (dynlink) { + func_t *memset_func = find_func("memset"); + if (!memset_func) + fatal("arm64 dynamic startup needs memset"); + a64_mov(0, A64_GP); + a64_mov(1, A64_ZR); + a64_mov_imm(2, global_frame); + a64_bl_addr(dynamic_sections.elf_plt_start + memset_func->plt_offset); + } + a64_b(GLOBAL_FUNC->bbs->elf_offset); + /* __syscall(number,arg1,...): AArch64 Linux wants x8,x0..x5. */ + if (!dynlink) { + a64_mov(8, 0); + a64_mov(0, 1); + a64_mov(1, 2); + a64_mov(2, 3); + a64_mov(3, 4); + a64_mov(4, 5); + a64_mov(5, 6); + emit(0xd4000001); + emit(0xd65f03c0); + } + for (ph2_ir_t *p = GLOBAL_FUNC->bbs->ph2_ir_list.head; p; p = p->next) + emit_ph2_ir(p); + if (MAIN_BB) { + if (dynlink) { + a64_mem(1, 8, 23, A64_SP, global_frame); + a64_mem(1, 8, 24, A64_SP, global_frame + 8); + a64_mov_imm(0, elf_code_start + MAIN_BB->elf_offset); + a64_mov(1, 23); + a64_mov(2, 24); + a64_mov(3, A64_ZR); + a64_mov(4, A64_ZR); + a64_mov(5, A64_ZR); + a64_mov(6, 25); + a64_bl_addr(dynamic_sections.elf_plt_start + PLT_FIXUP_SIZE); + emit(0xd4200000); /* __libc_start_main does not return */ + } else { + a64_mov(0, 23); + a64_mov(1, 24); + emit(0x94000000 | + (((MAIN_BB->elf_offset - elf_code->size) / 4) & 0x3ffffff)); + a64_mov_imm(8, 93); + emit(0xd4000001); + } + } + for (int i = 0; i < ph2_ir_idx; i++) + emit_ph2_ir(PH2_IR_FLATTEN[i]); + if (elf_code->size != elf_offset) + fatal("arm64 code-size accounting mismatch"); + if (dynlink) { + /* Dynamic addresses depend on the final text extent. The ARM64 stream + * is fixed-width, but .rodata may still have gained alignment padding + * since elf_preprocess(); rebuild the address-bearing tables + * immediately before writing PLT bytes. + */ + elf_rodata_start = elf_code_start + elf_code->size; + elf_layout_dynamic(); + elf_reset_dynamic_sections(); + elf_generate_dynamic_sections(); + plt_generate(); + } +} diff --git a/src/defs.h b/src/defs.h index c99fcc3e..3534f2fe 100644 --- a/src/defs.h +++ b/src/defs.h @@ -45,7 +45,17 @@ */ #define DUMP_INSN_LEN 512 #define MAX_TYPE_LEN 32 + +/* Declaration limit, and with MAX_ARGS_IN_REG it also sizes the outgoing + * stack-argument area every frame reserves (see add_func() in globals.c). A + * target passing many arguments in registers must raise it or that area is + * empty and a call with more arguments overwrites the caller's first locals. + * Raising it for everyone would widen func_t.param_defs and the variadic spill + * on targets that gain nothing, so each mk file states its own. + */ +#ifndef MAX_PARAMS #define MAX_PARAMS 8 +#endif #define MAX_LOCALS 3200 #define MAX_FIELDS 64 #define MAX_TYPES 256 @@ -177,6 +187,7 @@ #define ELF_MACHINE_ARM32 0x28 #define ELF_MACHINE_RV32 0xf3 #define ELF_MACHINE_X86_64 0x3e +#define ELF_MACHINE_AARCH64 0xb7 /* ELF class of the active target: a 64-bit pointer means ELF64, and every * 32-bit target means ELF32. Used to select the header/segment writers in @@ -690,6 +701,11 @@ struct ph2_ir { */ bool ofs_based_on_stack_top; bool is_pointer; /* True if this operation involves a pointer type */ + /* Operand provenance is required by LP64 backends: pointer arithmetic keeps + * the address operand wide but sign-extends an int index. + */ + bool src0_is_pointer; + bool src1_is_pointer; }; typedef struct ph2_ir ph2_ir_t; diff --git a/src/elf.c b/src/elf.c index 44043dc4..66ec0e34 100644 --- a/src/elf.c +++ b/src/elf.c @@ -155,8 +155,10 @@ void elf_write_jmprel(strbuf_t *buf, int offset, int sym_idx) #endif } -/* One GOT slot. */ -void elf_write_got_slot(strbuf_t *buf, int val) +/* One pointer-sized value: eight bytes on an ELF64 target, four on ELF32. Used + * for GOT slots and for any data-section object holding an address. + */ +void elf_write_ptr(strbuf_t *buf, int val) { #if ELF_IS_64 == 1 elf_write_quad(buf, val); @@ -171,6 +173,16 @@ void elf_write_got_slot(strbuf_t *buf, int val) */ void elf_layout_dynamic(void) { + /* Everything below is placed from the end of .rodata, and .got holds + * pointers, so that end has to be pointer-aligned. Padding here rather than + * in the caller keeps the two runs of this function in agreement -- the pad + * is already present the second time, so it is idempotent -- which a + * backend that bakes the PLT address into its calls, as AArch64's BL does, + * depends on. + */ + while ((elf_rodata_start + elf_rodata->size) % PTR_SIZE) + elf_write_byte(elf_rodata, 0); + int relplt_bytes = dynamic_sections.use_relaplt ? dynamic_sections.relaplt_size : dynamic_sections.relplt_size; @@ -1004,20 +1016,21 @@ void elf_generate_dynamic_sections(void) switch (ELF_MACHINE) { case ELF_MACHINE_ARM32: case ELF_MACHINE_X86_64: + case ELF_MACHINE_AARCH64: /* GOT[0] holds the address of .dynamic. The GOT is still being built, * so its final size comes from got_size rather than the buffer. */ - elf_write_got_slot(dynamic_sections.elf_got, - dynamic_sections.elf_got_start + - dynamic_sections.got_size + - dynamic_sections.elf_dynstr->size + - dynamic_sections.elf_dynsym->size); - elf_write_got_slot(dynamic_sections.elf_got, 0); - elf_write_got_slot(dynamic_sections.elf_got, 0); + elf_write_ptr(dynamic_sections.elf_got, + dynamic_sections.elf_got_start + + dynamic_sections.got_size + + dynamic_sections.elf_dynstr->size + + dynamic_sections.elf_dynsym->size); + elf_write_ptr(dynamic_sections.elf_got, 0); + elf_write_ptr(dynamic_sections.elf_got, 0); break; case ELF_MACHINE_RV32: - elf_write_got_slot(dynamic_sections.elf_got, 0); - elf_write_got_slot(dynamic_sections.elf_got, 0); + elf_write_ptr(dynamic_sections.elf_got, 0); + elf_write_ptr(dynamic_sections.elf_got, 0); break; } int got_idx = 0; @@ -1032,7 +1045,7 @@ void elf_generate_dynamic_sections(void) */ slot = dynamic_sections.elf_plt_start + PLT_FIXUP_SIZE + got_idx * PLT_ENT_SIZE + 6; - elf_write_got_slot(dynamic_sections.elf_got, slot); + elf_write_ptr(dynamic_sections.elf_got, slot); got_idx++; } @@ -1187,6 +1200,13 @@ void elf_preprocess(void) elf_code_start = ELF_START + elf_header_len; elf_rodata_start = elf_code_start + elf_offset; if (dynlink) { + /* Dynamic startup clears the synthetic global frame with memset. + * Reserve its PLT slot even when user code does not otherwise call it. + */ + func_t *memset_func = find_func("memset"); + if (memset_func) + memset_func->is_used = true; + /* Precalculate the sizes of .rel.plt (.rela.plt), .plt and .got * sections. * @@ -1242,9 +1262,19 @@ void elf_preprocess(void) } else { /* To prevent two load segments from sharing a common page, add PAGESIZE * to elf_data_start, since the first section of the second load segment - * is .data in static linking mode. + * is .data in static linking mode. ELF requires p_offset and p_vaddr to + * agree modulo p_align. ELF64 output pads the file to the next page + * before .data, so derive its virtual address from that same aligned + * file offset rather than merely adding a page to the preceding virtual + * end. */ +#if ELF_IS_64 == 1 + elf_data_start = + ELF_START + + ALIGN_UP(elf_header_len + elf_offset + elf_rodata->size, PAGESIZE); +#else elf_data_start = elf_rodata_start + elf_rodata->size + PAGESIZE; +#endif } elf_bss_start = elf_data_start + elf_data->size; elf_align(elf_symtab); @@ -1340,11 +1370,21 @@ void elf_generate(const char *outfile) if (!dynlink) { int ro_written = elf_header_len + elf_code->size + elf_rodata->size; int data_ofs = ALIGN_UP(ro_written, PAGESIZE); - char pad[PAGESIZE]; - for (int i = 0; i < PAGESIZE; i++) + /* Written in chunks rather than from one PAGESIZE-sized buffer: a + * target with a 64 KiB granule would otherwise put 64 KiB on the stack, + * and zero all of it to emit the few bytes actually needed. + */ + char pad[256]; + int left = data_ofs - ro_written; + + for (int i = 0; i < 256; i++) pad[i] = 0; - elf_write_all(fp, pad, data_ofs - ro_written); + while (left > 0) { + int n = left < 256 ? left : 256; + elf_write_all(fp, pad, n); + left -= n; + } } #endif /* Readable and writable sections */ @@ -1364,4 +1404,13 @@ void elf_generate(const char *outfile) elf_write_all(fp, elf_section_header->elements, elf_section_header->size); #endif fclose(fp); + + /* A generated ELF is meant to be runnable directly, but the mode it gets + * depends on which libc opened it, not on how the output is linked: the + * embedded lib/c.c passes 0775 to openat(2) while glibc's fopen("wb") + * yields 0666. Every compiler that reaches here -- host-built, static + * self-hosted, or dynamic -- therefore sets the bits explicitly. + */ + if (chmod((char *) outfile, 0x1ed) < 0) /* 0755 */ + usage_error("Unable to mark output executable"); } diff --git a/src/globals.c b/src/globals.c index c035b47c..dd89767b 100644 --- a/src/globals.c +++ b/src/globals.c @@ -11,6 +11,7 @@ #include #include #include +#include #include "defs.h" @@ -636,6 +637,8 @@ ph2_ir_t *add_ph2_ir(opcode_t op) */ ph2_ir->size_bytes = PTR_SIZE; ph2_ir->is_pointer = false; + ph2_ir->src0_is_pointer = false; + ph2_ir->src1_is_pointer = false; return add_existed_ph2_ir(ph2_ir); } @@ -1552,10 +1555,9 @@ void global_init(void) dynamic_sections.use_relaplt = false; break; case ELF_MACHINE_RV32: - dynamic_sections.use_relaplt = true; - break; case ELF_MACHINE_X86_64: - /* x86-64 uses RELA throughout. */ + case ELF_MACHINE_AARCH64: + /* Every target but Arm32 uses RELA throughout. */ dynamic_sections.use_relaplt = true; break; } @@ -1770,8 +1772,15 @@ __noreturn void fatal(const char *msg) /* abort() does not flush, so a diagnostic written to a pipe -- a build log, * or any invocation whose output is captured -- is discarded and the * compiler appears to die silently. + * + * The stream is NULL rather than stdout because a dynamically linked build + * resolves fflush through the PLT to the host libc, for which lib/c.h's + * 'stdout' -- the plain file descriptor 1 -- is not a FILE *. NULL means + * "every stream" there and is ignored by the unbuffered embedded libc, so + * it is right for both. That build needs the flush most, being the only one + * whose stdio actually buffers. */ - fflush(stdout); + fflush(NULL); abort(); } @@ -1782,7 +1791,7 @@ __noreturn void fatal(const char *msg) __noreturn void usage_error(const char *msg) { printf("[Error]: %s\n", msg); - fflush(stdout); + fflush(NULL); exit(1); } @@ -1801,7 +1810,7 @@ __noreturn void error_at(char *msg, source_location_t *loc) if (!loc) { printf("[Error]: %s\n", msg); - fflush(stdout); + fflush(NULL); exit(1); } @@ -1816,7 +1825,7 @@ __noreturn void error_at(char *msg, source_location_t *loc) */ if (!src) { printf("[Error]: %s\n", msg); - fflush(stdout); + fflush(NULL); exit(1); } @@ -1862,7 +1871,7 @@ __noreturn void error_at(char *msg, source_location_t *loc) strcpy(diagnostic + i, note); printf("%s\n", diagnostic); - fflush(stdout); /* exit() flushes, but say so once rather than rely on it */ + fflush(NULL); /* exit() flushes, but say so once rather than rely on it */ exit(1); } diff --git a/src/parser.c b/src/parser.c index 8a2062ee..1d710e1f 100644 --- a/src/parser.c +++ b/src/parser.c @@ -5612,7 +5612,11 @@ void parse_internal(void) { /* set starting point of global stack manually */ GLOBAL_FUNC = add_func("", true); - GLOBAL_FUNC->stack_size = 4; + + /* The first global slot retains the synthetic global-frame pointer. It must + * occupy a full target pointer, not the historic 32-bit word. + */ + GLOBAL_FUNC->stack_size = PTR_SIZE; GLOBAL_FUNC->bbs = arena_calloc(BB_ARENA, 1, sizeof(basic_block_t)); GLOBAL_FUNC->bbs->belong_to = GLOBAL_FUNC; /* Prevent nullptr deref in RA */ GLOBAL_FUNC->bbs->elf_offset = -1; /* not yet emitted */ @@ -5676,9 +5680,11 @@ void parse_internal(void) /* Add a global object to the .data section. * - * This object is used to save the global stack pointer. + * This object saves the global stack pointer, so it is written back as a + * pointer and must reserve a full one: on an LP64 target the historic + * 32-bit word left four bytes belonging to the next global. */ - elf_write_int(elf_data, 0); + elf_write_ptr(elf_data, 0); /* lexer initialization */ do { diff --git a/src/reg-alloc.c b/src/reg-alloc.c index 913646c6..3e38c15c 100644 --- a/src/reg-alloc.c +++ b/src/reg-alloc.c @@ -22,6 +22,36 @@ bool is_pointer_like(var_t *v) return v && (v->ptr_level > 0 || (v->type && v->type->ptr_level > 0)); } +/* An operand holds an address if it is pointer-like or is an array, which + * decays to one wherever it is used as a value. A subscript reaches OP_add with + * the array itself as the base, so leaving arrays out here would let an LP64 + * backend read a[i] as ordinary arithmetic and skip widening the index. + */ +bool is_address_like(var_t *v) +{ + return is_pointer_like(v) || (v && v->array_size > 0); +} + +/* Record which operands of a three-address instruction are addresses. + * + * An LP64 backend needs the operands apart, not just the instruction: pointer + * arithmetic keeps the address 64 bits wide while sign-extending the int index + * beside it. + * + * The two source flags and is_pointer deliberately ask different questions. + * Only the AArch64 backend reads the source flags, and it wants every address, + * arrays included. is_pointer is older and the x86-64 backend decides store + * widths and int narrowing by it, so it keeps counting pointer-like operands + * alone rather than acquiring arrays and changing a settled target. + */ +void set_ptr_flags(ph2_ir_t *ir, insn_t *insn) +{ + ir->src0_is_pointer = is_address_like(insn->rs1); + ir->src1_is_pointer = is_address_like(insn->rs2); + ir->is_pointer = is_pointer_like(insn->rd) || is_pointer_like(insn->rs1) || + is_pointer_like(insn->rs2); +} + /* Width of the value a local's frame slot actually holds. * * Slots are pointer-sized, but a scalar occupies only its low bytes. Reading @@ -186,6 +216,8 @@ ph2_ir_t *bb_add_ph2_ir(basic_block_t *bb, opcode_t op) n->ofs_based_on_stack_top = false; n->size_bytes = PTR_SIZE; /* default to the full slot; see add_ph2_ir */ n->is_pointer = false; + n->src0_is_pointer = false; + n->src1_is_pointer = false; if (!bb->ph2_ir_list.head) bb->ph2_ir_list.head = n; @@ -2103,6 +2135,7 @@ void reg_alloc_global(insn_t *global_insn) ir->src0 = src0; ir->src1 = src1; ir->dest = dest; + set_ptr_flags(ir, global_insn); break; } case OP_write: { @@ -2500,6 +2533,11 @@ void reg_alloc_bb(func_t *func, basic_block_t *bb) ir = bb_add_ph2_ir(bb, OP_branch); ir->src0 = src0; + + /* An LP64 backend tests an address over its full width and an int + * over its low word only. + */ + ir->src0_is_pointer = is_address_like(insn->rs1); ir->then_bb = bb->then_; ir->else_bb = bb->else_; break; @@ -2601,13 +2639,12 @@ void reg_alloc_bb(func_t *func, basic_block_t *bb) ir->src1 = src1; ir->dest = dest; - /* Record whether the result is an address. On LP64 an int-typed - * result has to wrap at 32 bits, while a pointer must keep all - * 64. The backend cannot tell the two apart without this. + /* Record whether the result is an address, and which operand it + * came from. On LP64 an int-typed result has to wrap at 32 bits, + * while a pointer must keep all 64, and pointer arithmetic has to + * widen the int index beside the address. */ - ir->is_pointer = is_pointer_like(insn->rd) || - is_pointer_like(insn->rs1) || - is_pointer_like(insn->rs2); + set_ptr_flags(ir, insn); break; case OP_negate: case OP_bit_not: @@ -2617,6 +2654,11 @@ void reg_alloc_bb(func_t *func, basic_block_t *bb) ir = bb_add_ph2_ir(bb, insn->opcode); ir->src0 = src0; ir->dest = dest; + + /* As for OP_branch: the width of the test follows the operand, not + * the result. + */ + ir->src0_is_pointer = is_address_like(insn->rs1); break; case OP_trunc: case OP_sign_ext: diff --git a/tests/arm64-abi.sh b/tests/arm64-abi.sh new file mode 100755 index 00000000..02a552fb --- /dev/null +++ b/tests/arm64-abi.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# Focused AAPCS64 integer ABI checks. These deliberately avoid host headers: +# shecc supplies its inlined libc and the same file can run for stage 0 or 2. +set -eu + +if [ "$#" -lt 1 ]; then + echo "Usage: $0 []" >&2 + exit 2 +fi + +# TARGET_EXEC is a command with its own arguments, so both it and the compiler +# invocation are kept as argv arrays. A string passed through eval would lose a +# checkout path containing a space. +read -r -a runner <<< "${TARGET_EXEC:-}" + +case "$1" in + 0) shecc=("$PWD/out/shecc") ;; + 2) shecc=("${runner[@]}" "$PWD/out/shecc-stage2.elf") ;; + *) + echo "arm64 ABI tests support stage 0 or 2" >&2 + exit 2 + ;; +esac + +if [ "${2:-0}" = 1 ]; then + shecc+=(--dynlink) + link_mode=dynamic +else + link_mode=static +fi + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT +count=0 + +run_case() +{ + name=$1 expected=$2 source=$3 + src="$tmpdir/test.c" exe="$tmpdir/test.elf" + printf '%s\n' "$source" > "$src" + if ! output=$("${shecc[@]}" -o "$exe" "$src" 2>&1); then + echo "FAIL: $name (compile)" >&2 + printf '%s\n' "$output" >&2 + exit 1 + fi + # A generated ELF must be runnable as produced, whichever libc opened it. + if [ ! -x "$exe" ]; then + echo "FAIL: $name (output is not executable)" >&2 + exit 1 + fi + set +e + "${runner[@]}" "$exe" > /dev/null 2>&1 + got=$? + set -e + if [ "$got" -ne "$expected" ]; then + echo "FAIL: $name (expected $expected, got $got)" >&2 + exit 1 + fi + count=$((count + 1)) +} + +# x0-x7 are the eight AAPCS64 integer/pointer argument registers. +run_case 'eight register arguments' 36 ' +int sum8(int a,int b,int c,int d,int e,int f,int g,int h) { + return a+b+c+d+e+f+g+h; +} +int main(void) { return sum8(1,2,3,4,5,6,7,8); }' + +# Arguments nine and ten must be read from the caller stack with 16-byte SP. +run_case 'overflow stack arguments' 55 ' +int sum10(int a,int b,int c,int d,int e,int f,int g,int h,int i,int j) { + return a+b+c+d+e+f+g+h+i+j; +} +int main(void) { return sum10(1,2,3,4,5,6,7,8,9,10); }' + +# The callee reads stack arguments at a fixed offset above its own frame, so +# that offset has to be rounded exactly as the prologue rounds the frame. A +# frame whose size is 8 modulo 16 is what catches a disagreement: an +# address-taken local forces the rounding that flips the parity. +run_case 'stack arguments with an odd-parity frame' 42 ' +int pick(int a,int b,int c,int d,int e,int f,int g,int h,int i) { + int v; int *p = &v; *p = 0; + return i; +} +int main(void) { return pick(1,2,3,4,5,6,7,8,42); }' + +run_case 'stack arguments read past live locals' 38 ' +int sum10(int a,int b,int c,int d,int e,int f,int g,int h,int i,int j) { + int t[3]; t[0]=a; t[1]=i; t[2]=j; + return t[0]+t[1]+t[2]+i+j; +} +int main(void) { + int keep = 5; int *q = &keep; + return sum10(1,2,3,4,5,6,7,8,9,10) - *q + 4; +}' + +# SP must stay 16-byte aligned: AArch64 Linux enables SP alignment checking, so +# a frame rounded to 8 faults the moment the next prologue touches [sp]. +run_case 'stack stays 16-byte aligned through nesting' 42 ' +int d3(int x) { int a; int *p = &a; *p = x; return *p + 1; } +int d2(int x) { int a,b,c; a=x; b=a+1; c=b+1; return d3(c) + 1; } +int d1(int x) { int a; int *p = &a; *p = x; return d2(*p) + 1; } +int main(void) { return d1(37); }' + +# Narrow array elements must load sign-extended. Kept here rather than in +# tests/driver.sh because the Arm backend zero-extends them (see TODO.md). +run_case 'narrow array load and store' 42 ' +int main(void) { + char b[4]; short h[4]; int i; + for (i = 0; i < 4; i++) { b[i] = -1 - i; h[i] = -1000 - i; } + for (i = 0; i < 4; i++) { + if (b[i] != -1 - i) return 1; + if (h[i] != -1000 - i) return 2; + } + return 42; +}' + +# x19-x28 are callee-saved; the backend currently allocates x20-x22. +run_case 'callee-saved allocation survives call' 42 ' +int leaf(int x) { int a=7,b=8,c=9; return x+a+b+c; } +int caller(int x) { int keep=18; return leaf(x)+keep; } +int main(void) { return caller(0); }' + +run_case 'struct member offset layout' 12 ' +struct P { char tag; int number; }; +int main(void) { struct P p; p.tag=1; p.number=11; return p.tag+p.number; }' + +run_case 'defined function pointer' 42 ' +int add1(int n) { return n + 1; } +int main(void) { + int (*p)(int); + p = add1; + return p(41); +}' + +run_case 'negative pointer index' 7 ' +int main(void) { + int a[3]; + int *p = &a[1]; + a[0] = 7; + return p[-1]; +}' + +run_case 'negative runtime pointer subtraction' 42 ' +int get(int i) { + int a[3]; + int *p = &a[1]; + a[2] = 42; + return *(p - i); +} +int main(void) { return get(-1); }' + +if [ "$link_mode" = static ]; then + + # Linux AArch64 getpid is syscall 172. This verifies the backend moves the + # syscall number to x8 and six arguments to x0..x5. + run_case 'syscall register ABI' 0 ' +int main(void) { + return __syscall(172, 0, 0, 0, 0, 0, 0) > 0 ? 0 : 1; +}' +fi + +if [ "$link_mode" = dynamic ]; then + run_case 'external function pointer through PLT' 0 ' +int puts(char *s); +int main(void) { + int (*p)(char *); + p = puts; + p("external function pointer"); + return 0; +}' +fi + +echo "AAPCS64 ABI: $count/$count passed (stage $1, $link_mode)" diff --git a/tests/driver.sh b/tests/driver.sh index 08a9dd45..0f3afd66 100755 --- a/tests/driver.sh +++ b/tests/driver.sh @@ -648,6 +648,102 @@ declare -a variable_tests=( run_items_tests variable_tests +# Narrow signed values must stay negative through promotion and through a store +# and reload. An LP64 backend holds them in a 64-bit register, so a load that +# zero-extends or a promotion that forgets to extend turns a small negative +# number into a large positive one. The array-element form belongs here too, but +# it fails on the Arm backend, whose char elements load zero-extended, so it +# stays in tests/arm64-abi.sh until that is fixed. +try_ 42 << EOF +int main() { + char c = -5; + short s = -1000; + int ci = c; + int si = s; + if (ci != -5) + return 1; + if (si != -1000) + return 2; + if (c >= 0) + return 3; + if (s >= 0) + return 4; + return 42; +} +EOF + +# A pointer is wider than an int on an LP64 target, so testing one for truth has +# to consider the whole value, not just its low word. No fixture can force the +# case that separates the two, since pinning a pointer whose low word is zero +# needs a 64-bit literal and shecc has no integer constant that wide. What is +# testable is that every path which tests an address agrees: the backend emits a +# different width for a branch, for a logical negation and for a comparison, so +# each is reached here with a null and a non-null pointer. +try_ 42 << EOF +struct holder { + int *ptr; +}; + +int *pick(int *p, int take) +{ + if (take) + return p; + return 0; +} + +int main() { + int v = 42; + int *p = &v; + int *n = 0; + struct holder h; + int seen = 0; + + if (!p) + return 1; + if (n) + return 2; + if (p == 0) + return 3; + if (n != 0) + return 4; + + while (n) + return 5; + + seen = p ? 1 : 0; + if (!seen) + return 6; + seen = n ? 1 : 0; + if (seen) + return 7; + + if (p && !n) + seen = 2; + if (seen != 2) + return 8; + if (n || !p) + return 9; + + /* A pointer that reaches the test through a return value or a struct + * field has been through a store and a reload on the way. + */ + if (!pick(p, 1)) + return 10; + if (pick(p, 0)) + return 11; + + h.ptr = n; + if (h.ptr) + return 12; + h.ptr = p; + if (!h.ptr) + return 13; + + int *q = h.ptr; + return *q; +} +EOF + # Category: Compound Literals begin_category "Compound Literals" "Testing C99 compound literal features"