ffmasm is an assembler for hand-assembling from Java.
It uses Foreign Function & Memory API, so the application can call assembled code via MethodHandle.
- Javadoc: https://yasuenag.github.io/ffmasm/
- Maven package: https://github.com/YaSuenag/ffmasm/packages/
- Supported instructions: See builder classes in com.yasuenag.ffmasm.amd64.
Java 22
- Linux AMD64
- Windows AMD64
- Linux AArch64
$ mvn package
$ mvn testSee Javadoc and cpumodel examples.
CodeSegment is a storage for assembled code. In Linux, it would be allocated by mmap(2) with executable bit.
It implements AutoCloseable, so you can use try-with-resources in below:
try(var seg = new CodeSegment()){
...
}You can assemble the code via inner classes of com.yasuenag.ffmasm.AsmBuilder:
You need to use com.yasuenag.ffmasm.amd64.Register to specify registers in assembly code. Following example shows how you can create method (I)I (JNI signature) in ffmasm:
You can get MethodHandle in result of build().
var desc = FunctionDescriptor.of(
ValueLayout.JAVA_INT, // return value
ValueLayout.JAVA_INT // 1st argument
);
var method = new AsmBuilder.AMD64(seg, desc)
/* push %rbp */ .push(Register.RBP)
/* mov %rsp, %rbp */ .movRM(Register.RSP, Register.RBP, OptionalInt.empty())
/* mov %rdi, %rax */ .movRM(Register.RDI, Register.RAX, OptionalInt.empty())
/* leave */ .leave()
/* ret */ .ret()
.build(Linker.Option.critical(false));Note
Linker.Option.critical() is recommended to pass build() method due to performance, but it might be cause of some issues in JVM (time to synchronize safepoint, memory corruption, etc). See Javadoc of critical().
Most of code is same with AMD64, but it is the only one difference to use AsmBuilder.AArch64 as builder instance.
var desc = FunctionDescriptor.of(
ValueLayout.JAVA_INT, // return value
ValueLayout.JAVA_INT // 1st argument
);
var method = new AsmBuilder.AArch64(codeSegment, desc)
/* stp x29, x30, [sp, #-16]! */ .stp(Register.X29, Register.X30, Register.SP, IndexClass.PreIndex, -16)
/* mov x29, sp */ .mov(Register.X29, Register.SP)
/* ldp x29, x30, [sp], #16 */ .ldp(Register.X29, Register.X30, Register.SP, IndexClass.PostIndex, 16)
/* ret */ .ret(Optional.empty())
.build();int ret = (int)method.invoke(100); // "ret" should be 100ffmasm-disassembler can disassemble the code in MemorySegment like generated by ffmasm, and dump assembly code to stdout.
You can download ffmasm-dissassembler Maven package from GitHub packages: https://github.com/YaSuenag/ffmasm/packages/2370043
See examples/disas for details.
ffmasm-disassembler requires hsdis.
To generate hsdis for Linux, you can use hsdis-builder.
It should be deployed one of following directory (it is documented as source comment in disassembler.cpp in HotSpot):
$JAVA_HOME/lib/<vm>/libhsdis-<arch>.so$JAVA_HOME/lib/<vm>/hsdis-<arch>.so$JAVA_HOME/lib/hsdis-<arch>.sohsdis-<arch>.so(usingLD_LIBRARY_PATH)
If you don't want to deploy hsdis into your JDK, you can specify hsdis system property like -Dhsdis=/path/to/hsdis-amd64.so
import com.yasuenag.ffmasmtools.disas.Disassembler;
: <snip>
MemorySegment rdtsc = createRDTSC(); // Generate machine code with ffmasm
Disassembler.dumpToStdout(rdtsc); // Dump assembly code of `rdtsc` to stdout
You can bind native method to MemorySegment of ffmasm code dynamically.
You have to construct MemorySegment of the machine code with AMD64AsmBuilder, and you have to get it from getMemorySegment(). Then you can bind it via NativeRegister.
Following example shows native method test is binded to the code made by ffmasm. Note that 1st argument in Java is located at arg3 in native function because this is native function (1st arg is JNIEnv*, and 2nd arg is jobject or jclass).
public native int test(int arg);
<snip>
try(var seg = new CodeSegment()){
var desc = FunctionDescriptor.of(
ValueLayout.JAVA_INT, // return value
ValueLayout.JAVA_INT, // 1st arg (JNIEnv *)
ValueLayout.JAVA_INT, // 2nd arg (jobject)
ValueLayout.JAVA_INT // 3rd arg (arg1 of caller)
);
var stub = AsmBuilder.AMD64(seg, desc)
/* push %rbp */ .push(Register.RBP)
/* mov %rsp, %rbp */ .movRM(Register.RBP, Register.RSP, OptionalInt.empty())
/* mov %arg3, retReg */ .movMR(argReg.arg3(), argReg.returnReg(), OptionalInt.empty()) // arg1 in Java is arg3 in native
/* leave */ .leave()
/* ret */ .ret()
.getMemorySegment();
var method = this.getClass()
.getMethod("test", int.class);
var methodMap = Map.of(method, stub);
var register = NativeRegister.create(this.getClass());
register.registerNatives(methodMap);
final int expected = 100;
int actual = test(expected);
Assertions.assertEquals(expected, actual);
}JVMCI is not FFM, but ffmasm supports it!
You can install your machine code into CodeCache on HotSpot as Tier 4 compiled code via JVMCI.
public static int getPid(){
throw new UnsupportedOperationException("This method should be overriden by jvmci-adapter in ffmasm");
}
private static void installAMD64Code(Method method) throws Exception{
new JVMCIAMD64AsmBuilder()
.emitPrologue()
/* mov %rax, $39 */ .movImm(com.yasuenag.ffmasm.amd64.Register.RAX, 39) // getpid
/* syscall */ .syscall()
.emitEpilogue()
.install(method, 16);
}
private static void installAArch64Code(Method method) throws Exception{
new JVMCIAArch64AsmBuilder()
.emitPrologue()
/* movz x8, $172 */ .movz(com.yasuenag.ffmasm.aarch64.Register.X8, 172, HWShift.None) // getpid
/* svc #0 */ .svc(0)
.emitEpilogue()
.install(method, 16);
}You need to use JVMCIAMD64AsmBuilder or its family (for SSE, AVX like AMD64AsmBuilder) for AMD64, JVMCIAArch64AsmBuilder for AArch64. Note that you have to call both emitPrologue() and emitEpilogue() before install().
In above case, install() in each builder classes override getPid(), so you wouldn't see UnsupportedOperationException and you can get PID from it without any error.
Tip
Register usage in C2 compiler is different from platform ABI (e.g. AMD64 System V ABI), and JVMCI code should compliant this rule. See CallingSequences on OpenJDK Wiki for details.
These builder classes are provided by jvmci-adapter. You can depend it on pom.xml as following:
<dependencies>
<dependency>
<groupId>com.yasuenag</groupId>
<artifactId>jvmci-adapter</artifactId>
<version>0.1.0</version>
</dependency>
</dependencies>jvmci-adapter depends on ffmasm, and it is exposed transitively, so you do not need to add dependency to ffmasm.
You can record both function name and entry point address as a perf map file.
You can pass function name into build() method:
.build("GeneratedFunc", Linker.Option.critical(true));Function name would be set to <unnamed> if you do not pass function name (includes calling build(Linker.Option)).
perf map file would be written at shutdown hook when CodeSegment lives. All of functions in all of CodeSegments which are lives would be dumped at the time of shutdown hook.
You need to enable perf map dumper via CodeSegment::enablePerfMapDumper. Call CodeSegment::disablePerfMapDumper if you want to cancel the dumper.
perf tool on Linux supports JIT-generated code. ffmasm can dump generated code as a jitdump. See an example for details.
Pass JitDump insntace to build method.
jitdump = JitDump.getInstance(Path.of("."));
:
.build("GeneratedFunc", jitdump);Then you can run perf record. Note that you have to set monotonic clock with -k option.
perf record -k 1 $JAVA_HOME/bin/java ...
As a result, you would get jit-<PID>.dump which includes JIT information. You should keep until run perf inject.
perf.data generated by perf record would not include JIT'ed code, so you need to inject them via perf inject as following.
perf inject --jit -i perf.data -o perf.jit.data
You will get some .so file and perf.jit.data as an injected file as a result.
perf report -i perf.jit.data
The GNU Lesser General Public License, version 3.0