diff options
| author | Cliff Click <cliffc@acm.org> | 2026-10-19 14:56:14 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-10-19 14:56:14 -0700 |
| commit | 7edca9823ec8ef0aed10c88fa07ef969792aab77 (patch) | |
| tree | d95e308cac7a9c5039a59be636d881982a9b4878 /chapter25 | |
| parent | 3617495d8efbcce23bbc9f1754c7d27d34d30a52 (diff) | |
| download | Simple-7edca9823ec8ef0aed10c88fa07ef969792aab77.tar.gz Simple-7edca9823ec8ef0aed10c88fa07ef969792aab77.zip | |
Split from ch25, SCCP and interprocedural SCCPmain
Diffstat (limited to 'chapter25')
| -rw-r--r-- | chapter25/README.md | 18 | ||||
| -rw-r--r-- | chapter25/ROADMAP.md | 20 | ||||
| -rw-r--r-- | chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java | 46 | ||||
| -rw-r--r-- | chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java | 4 | ||||
| -rw-r--r-- | chapter25/src/test/java/com/seaofnodes/simple/StaticDataTest.java | 47 |
5 files changed, 129 insertions, 6 deletions
diff --git a/chapter25/README.md b/chapter25/README.md index 8e51c557..bbe0c50e 100644 --- a/chapter25/README.md +++ b/chapter25/README.md @@ -52,6 +52,24 @@ these addresses through the in-memory linker. Thread-local variables and C macro need accessor functions: the system library exposes `libc.errno()` through a small native runtime accessor. +Native function pointers still occupy four bytes. A native library can live +above that address range, so the AMD64 ELF writer gives each address-taken +native function a small entry in Simple's low-address code. The entry tail-jumps +through the native symbol's PLT entry; arguments and the return address pass +through unchanged. Direct calls still use the native symbol directly. + +Static class fields and addresses computed in code both refer to that entry. +Its stable weak symbol, `__simple_native$<name>`, lets separately compiled +objects agree on one pointer when linked into an executable. The class image +uses a four-byte relocation to this defined entry, instead of a four-byte +relocation to a dynamic native symbol in writable data, which GNU ld rejects. +Constant field Stores can still fold away. The entry is a Simple function +identity; it need not equal the raw address obtained by native code. + +`StaticDataTest.testNativeFunctionIdentity` reads two separately compiled class +images before their initializers run, compares their stored and computed +addresses, and calls the native function. It checks both object-link orders. + ## Object files contain ideal IR The ELF writer stores native code and a `.simple` section containing canonical diff --git a/chapter25/ROADMAP.md b/chapter25/ROADMAP.md index 966bf8e9..b5647587 100644 --- a/chapter25/ROADMAP.md +++ b/chapter25/ROADMAP.md @@ -487,6 +487,26 @@ Mutable fields may have static initial contents without becoming constants. Choose read-only storage only when no runtime writes remain. Static reference cycles can use relocations without requiring a dynamic initialization cycle. +For a wholly immutable, pre-initialized class, eliminate the mutable `$init` +state as well as the checks so the class image can reside in ROM. The native +function thunks preserve static initialization of function fields. + +### Compact code and data arenas + +Keep four-byte Simple references into bounded code and data arenas; the arenas +need not overlap or live near zero. Choose absolute addresses versus offsets +from an arena base when implementing placement and indirect calls. Native +function thunks can occupy the code arena while native libraries live elsewhere. +The current thunk implementation uses the existing low-address executable; +ordinary data pointers still occupy eight bytes. + +Give full-width native pointers separate types and foreign lifetime contracts. +Opaque device addresses may need to be passed through without CPU dereference. +Simple object lifetimes should be managed automatically without a GC; +ownership, borrowing, cycles, and native retention still need design. Compact +storage alone does not establish those rules. Preserve static class images and +explicit native boundary conversions when choosing the arena representation. + ### CFG frequency estimates After lowering and loop-tree construction, estimate block frequencies for GCM diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java index b3eac583..5a280723 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/ElfWriter.java @@ -1,6 +1,7 @@ package com.seaofnodes.simple.codegen; import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeFunPtr; import com.seaofnodes.simple.type.TypeStruct; import com.seaofnodes.simple.util.Ary; import com.seaofnodes.simple.util.BAOS; @@ -171,7 +172,7 @@ public class ElfWriter { public int symbol( String name, int shndx, int bind, int type, long value, long size ) { int gidx = gidx(); // Global index before I write to _globals - BAOS baos = bind==SYM_BIND_GLOBAL ? _globals : _contents; + BAOS baos = bind==SYM_BIND_LOCAL ? _contents : _globals; write4(baos,_strtab.writeCString(name)); int info = (bind << 4) | (type & 0xF); baos.write(info); @@ -179,7 +180,7 @@ public class ElfWriter { write2(baos,shndx); // section index write8(baos,value); write8(baos,size); - return bind==SYM_BIND_GLOBAL ? gidx : -99; + return bind==SYM_BIND_LOCAL ? -99 : gidx; } public int symbol( String name, int shndx, int bind, int type ) { @@ -265,6 +266,7 @@ public class ElfWriter { public static final int SYM_BIND_LOCAL = 0; public static final int SYM_BIND_GLOBAL = 1; + public static final int SYM_BIND_WEAK = 2; public static final int SYM_TYPE_NOTYPE = 0; public static final int SYM_TYPE_OBJECT = 1; @@ -281,6 +283,26 @@ public class ElfWriter { // ------------------------------------------------------------------------ + // Native code can live above 4G, outside our four-byte function pointers. + // Give address-taken natives a low entry which tail-jumps through the PLT. + // A weak, stable name makes references from separate objects agree on one + // entry. Do not patch references straight to the local copy: that would + // bypass the linker's choice and break pointer equality across objects. + private int nativeEntry(String name, HashMap<String,Integer> entries, + DataSection text, SymbolSection symbols, ReloSection relos) { + Integer old = entries.get(name); + if( old!=null ) return old; + int offset = text.size(); + text._contents.write(0xE9); // jmp rel32; preserve arguments and return address + text.write4(0); + int target = symbols.symbol(name,0,SYM_BIND_GLOBAL,SYM_TYPE_FUNC); + relos.writeReloOffPlus(offset+1,target,(byte)4/*PLT32*/); + int entry = symbols.symbol("__simple_native$"+name,text._index, + SYM_BIND_WEAK,SYM_TYPE_FUNC,offset,5); + entries.put(name,entry); + return entry; + } + public ElfWriter export(boolean emitEntrySymbol) { if( _code._serial==null ) throw new IllegalStateException("ELF export needs ideal IR: request Export from the driver, or Serialize before Lower"); @@ -300,7 +322,10 @@ public class ElfWriter { // we've already constructed these entire sections in the encoding phase Encoding enc = _code._encoding; - DataSection text = new DataSection(".text" , Section.SHT_PROGBITS, enc._bits , DataSection.SHF_ALLOC | DataSection.SHF_WRITE | DataSection.SHF_EXECINSTR ); + // Append ELF-only thunks to a copy. Export must not change instruction + // offsets or the image used by the in-memory linker and emulators. + BAOS textBits = new BAOS(enc._bits.toByteArray(),enc._bits.size()); + DataSection text = new DataSection(".text" , Section.SHT_PROGBITS, textBits, DataSection.SHF_ALLOC | DataSection.SHF_WRITE | DataSection.SHF_EXECINSTR ); // Constant pool DataSection rodata = new DataSection(".rodata", Section.SHT_PROGBITS, enc._cpool, DataSection.SHF_ALLOC ); // Static/global pool @@ -316,12 +341,19 @@ public class ElfWriter { // create external .text relocations ReloSection relocations = new ReloSection(text._index); + HashMap<String,Integer> nativeEntries = new HashMap<>(); for( Node n : enc._externals.keySet()) { String extern = enc._externals.get(n); - int symidx = symbols.symbol(extern, 0, SYM_BIND_GLOBAL, SYM_TYPE_NOTYPE); + boolean nativeAddress = n instanceof FunPtrNode && + _code.externFunc(((TypeFunPtr)n._type).fidx())!=null; + int symidx = nativeAddress + ? nativeEntry(extern,nativeEntries,text,symbols,relocations) + : symbols.symbol(extern,0,SYM_BIND_GLOBAL,SYM_TYPE_FUNC); int offset = enc.opStart(n) + enc.opLen(n) - 4; - relocations.writeReloOffPlus(offset, symidx, (byte)4/*PLT32*/); + // Address materialization and static fields name the same thunk; + // direct calls still go straight to the native symbol's PLT entry. + relocations.writeReloOffPlus(offset,symidx,(byte)(nativeAddress ? 2/*PC32*/ : 4/*PLT32*/)); } // Native ELF output is AMD64. Data addresses use PC32, not PLT calls. @@ -353,7 +385,9 @@ public class ElfWriter { ReloSection rwRelos=new ReloSection(".rela.data",rwdata._index); for( StaticData.Relocation relo : enc._data.relocations ) { String name=enc._data.targetSymbol(relo.value()); - Integer sym=dataSymbols.get(name); + String nativeName=relo.value() instanceof TypeFunPtr fp ? _code.externFunc(fp.fidx()) : null; + Integer sym=nativeName==null ? dataSymbols.get(name) + : nativeEntry(nativeName,nativeEntries,text,symbols,relocations); if( sym==null ) { sym=symbols.symbol(name,0,SYM_BIND_GLOBAL,SYM_TYPE_NOTYPE); dataSymbols.put(name,sym); diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java index 8e9b99db..06bf14f5 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/Encoding.java @@ -171,6 +171,10 @@ public class Encoding implements com.seaofnodes.isa.CodeSink { // Code address as a constant public Encoding relo( FunPtrNode fptr ) { TypeFunPtr tfp = (TypeFunPtr)fptr._type; + // Native functions have no local FunNode, but are still callable. + // Do not treat their addresses as identities of deleted functions. + String nativeName = _code.externFunc(tfp.fidx()); + if( nativeName!=null ) return external(fptr,nativeName); FunNode fun = _code.link(tfp); if( fun != null && !_code.owns(fun) ) _externals.put(fptr,fun._name); diff --git a/chapter25/src/test/java/com/seaofnodes/simple/StaticDataTest.java b/chapter25/src/test/java/com/seaofnodes/simple/StaticDataTest.java index 03df05a7..88d0246d 100644 --- a/chapter25/src/test/java/com/seaofnodes/simple/StaticDataTest.java +++ b/chapter25/src/test/java/com/seaofnodes/simple/StaticDataTest.java @@ -93,6 +93,53 @@ public class StaticDataTest { assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe)); } + // Native entries must agree too, both in static fields (before <clinit>) + // and when code materializes the address. The native body may live above 4G. + @Test public void testNativeFunctionIdentity() throws Exception { + Path dir=Path.of("build/objs/static-native-functions"); + Files.createDirectories(dir); + int[] offsets=new int[2]; + for( int i=0; i<2; i++ ) { + String name=i==0 ? "A" : "B"; + String src="{ i32 -> i32 } abs = \"C\"; val get"+name+"={ -> abs; }; return 0;"; + CodeGen code=new CodeGen(null,dir.toString(),null,name,src,123L,true,TypeInteger.BOT) + .driver(CodeGen.Phase.TypeCheck); + assertNull("The native address is initialized in the static image", + code._stop.walk(n -> n instanceof StoreNode st && st._name.equals("abs") ? n : null)); + code.driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + for( var obj : code._encoding._data.entries() ) + if( obj.symbol.equals(name+".$class") ) { + TypeStruct ts=(TypeStruct)obj.type; + offsets[i]=ts.offset(ts.find("abs")); + } + } + Path driver=dir.resolve("driver.c"); + Files.writeString(driver,""" + #include <stdint.h> + #include <string.h> + extern unsigned char a[] __asm__("A.$class"), b[] __asm__("B.$class"); + typedef int32_t (__attribute__((CALL_CONV)) *fun)(int32_t); + __attribute__((CALL_CONV)) extern fun getA(void), getB(void); + int main(void) { + uint32_t x,y; + memcpy(&x,a+%d,sizeof(x)); + memcpy(&y,b+%d,sizeof(y)); + fun f=(fun)(uintptr_t)x; + if(!x || x!=y || f!=getA() || f!=getB()) return 1; + return f(-7)!=7; + } + """.formatted(offsets[0],offsets[1])); + String exe=dir.resolve(TestC.OS.startsWith("Windows")?"test.exe":"test").toString(); + String conv=TestC.CALL_CONVENTION.equals("win64")?"ms_abi":"sysv_abi"; + // Either object's weak definition can win; every reference must agree. + for( String first : new String[]{"A","B"} ) { + String second=first.equals("A")?"B":"A"; + TestC.linkExe(dir.resolve(first+".o").toString(),conv,driver.toString(), + new Ary<>(new String[]{dir.resolve(second+".o").toString()}),exe); + assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe)); + } + } + // Two separately compiled users must reference the same mutable class object. @Test public void testSharedClass() throws Exception { Path dir=Path.of("build/objs/static-data-shared"); |
