This commit is contained in:
parent
dbc865ee67
commit
562c839835
37 changed files with 742 additions and 45 deletions
27
docs/pe.md
Normal file
27
docs/pe.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Portable Executable
|
||||
|
||||
## Notes
|
||||
|
||||
Unlike Linux, Windows does not ignore zero-length sections at the end of a file and will
|
||||
fail loading them because they don't exist within the file. Adding a single byte to the
|
||||
section can fix this problem, but it's easier to just remove the section header entirely.
|
||||
The solution used here is to guarantee that the data section is never empty by always
|
||||
importing a few core functions from "kernel32.dll".
|
||||
|
||||
## DLL function pointers
|
||||
|
||||
The section where the DLL function pointers are stored does not need to be marked as writable.
|
||||
The Windows executable loader resolves the pointers before they are loaded into memory.
|
||||
|
||||
The stack must be 16 byte aligned before a DLL function is called.
|
||||
|
||||
## Links
|
||||
|
||||
- https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
|
||||
- https://learn.microsoft.com/en-us/previous-versions/ms809762(v=msdn.10)
|
||||
- https://learn.microsoft.com/en-us/archive/msdn-magazine/2002/february/inside-windows-win32-portable-executable-file-format-in-detail
|
||||
- https://learn.microsoft.com/en-us/archive/msdn-magazine/2002/march/inside-windows-an-in-depth-look-into-the-win32-portable-executable-file-format-part-2
|
||||
- https://blog.kowalczyk.info/articles/pefileformat.html
|
||||
- https://keyj.emphy.de/win32-pe/
|
||||
- https://corkamiwiki.github.io/PE
|
||||
- https://github.com/ayaka14732/TinyPE-on-Win10
|
|
@ -1,28 +1,23 @@
|
|||
init() {
|
||||
// kernel32.SetConsoleCP(cp.utf8)
|
||||
// kernel32.SetConsoleOutputCP(cp.utf8)
|
||||
utf8 := 65001
|
||||
kernel32.SetConsoleCP(utf8)
|
||||
kernel32.SetConsoleOutputCP(utf8)
|
||||
main.main()
|
||||
exit()
|
||||
}
|
||||
|
||||
exit() {
|
||||
// kernel32.ExitProcess(0)
|
||||
kernel32.ExitProcess(0)
|
||||
}
|
||||
|
||||
crash() {
|
||||
// kernel32.ExitProcess(1)
|
||||
kernel32.ExitProcess(1)
|
||||
}
|
||||
|
||||
// const {
|
||||
// cp {
|
||||
// utf8 65001
|
||||
// }
|
||||
// }
|
||||
|
||||
// extern {
|
||||
// kernel32 {
|
||||
// SetConsoleCP(cp uint)
|
||||
// SetConsoleOutputCP(cp uint)
|
||||
// ExitProcess(code uint)
|
||||
// }
|
||||
// }
|
||||
extern {
|
||||
kernel32 {
|
||||
SetConsoleCP(cp uint)
|
||||
SetConsoleOutputCP(cp uint)
|
||||
ExitProcess(code uint)
|
||||
}
|
||||
}
|
|
@ -1,3 +1,10 @@
|
|||
write(_ []byte) -> (written int) {
|
||||
return 0
|
||||
}
|
||||
|
||||
extern {
|
||||
kernel32 {
|
||||
GetStdHandle(handle int64) -> int64
|
||||
WriteConsoleA(fd int64, buffer *byte, length uint32, written *uint32) -> bool
|
||||
}
|
||||
}
|
|
@ -42,6 +42,7 @@ const (
|
|||
)
|
||||
|
||||
var CPU = cpu.CPU{
|
||||
Call: []cpu.Register{X0, X1, X2, X3, X4, X5, X6},
|
||||
Syscall: []cpu.Register{X8, X0, X1, X2, X3, X4, X5},
|
||||
}
|
||||
Call: []cpu.Register{X0, X1, X2, X3, X4, X5, X6},
|
||||
Syscall: []cpu.Register{X8, X0, X1, X2, X3, X4, X5},
|
||||
ExternCall: []cpu.Register{X0, X1, X2, X3, X4, X5, X6, X7},
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"git.urbach.dev/cli/q/src/build"
|
||||
"git.urbach.dev/cli/q/src/data"
|
||||
"git.urbach.dev/cli/q/src/dll"
|
||||
"git.urbach.dev/cli/q/src/elf"
|
||||
"git.urbach.dev/cli/q/src/exe"
|
||||
)
|
||||
|
@ -13,6 +14,7 @@ import (
|
|||
type Assembler struct {
|
||||
Data data.Data
|
||||
Instructions []Instruction
|
||||
Libraries dll.List
|
||||
}
|
||||
|
||||
// Append adds another instruction.
|
||||
|
@ -30,7 +32,7 @@ func (a *Assembler) Last() Instruction {
|
|||
}
|
||||
|
||||
// Compile compiles the instructions to machine code.
|
||||
func (a *Assembler) Compile(b *build.Build) (code []byte, data []byte) {
|
||||
func (a *Assembler) Compile(b *build.Build) (code []byte, data []byte, libs dll.List) {
|
||||
data, dataLabels := a.Data.Finalize()
|
||||
|
||||
c := compiler{
|
||||
|
@ -38,6 +40,7 @@ func (a *Assembler) Compile(b *build.Build) (code []byte, data []byte) {
|
|||
data: data,
|
||||
dataLabels: dataLabels,
|
||||
labels: make(map[string]Address, 32),
|
||||
libraries: a.Libraries,
|
||||
}
|
||||
|
||||
switch b.Arch {
|
||||
|
@ -57,18 +60,22 @@ func (a *Assembler) Compile(b *build.Build) (code []byte, data []byte) {
|
|||
}
|
||||
|
||||
x := exe.New(elf.HeaderEnd, b.FileAlign, b.MemoryAlign)
|
||||
x.InitSections(c.code, c.data)
|
||||
x.InitSections(c.code, c.data, nil)
|
||||
dataSectionOffset := x.Sections[1].MemoryOffset - x.Sections[0].MemoryOffset
|
||||
|
||||
for dataLabel, address := range dataLabels {
|
||||
c.labels[dataLabel] = dataSectionOffset + address
|
||||
}
|
||||
|
||||
if b.OS == build.Windows {
|
||||
c.importsStart = x.Sections[2].MemoryOffset - x.Sections[0].MemoryOffset
|
||||
}
|
||||
|
||||
for _, call := range c.deferred {
|
||||
call()
|
||||
}
|
||||
|
||||
return c.code, c.data
|
||||
return c.code, c.data, c.libraries
|
||||
}
|
||||
|
||||
// Merge combines the contents of this assembler with another one.
|
||||
|
@ -81,6 +88,12 @@ func (a *Assembler) Merge(b *Assembler) {
|
|||
|
||||
a.Instructions = append(a.Instructions, b.Instructions[skip:]...)
|
||||
maps.Copy(a.Data, b.Data)
|
||||
|
||||
for _, library := range b.Libraries {
|
||||
for _, fn := range library.Functions {
|
||||
a.Libraries = a.Libraries.Append(library.Name, fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetData sets the data for the given label.
|
||||
|
|
|
@ -37,9 +37,9 @@ func TestAssembler(t *testing.T) {
|
|||
final.Merge(b)
|
||||
final.Merge(c)
|
||||
|
||||
code, _ := final.Compile(&build.Build{Arch: build.ARM})
|
||||
code, _, _ := final.Compile(&build.Build{Arch: build.ARM})
|
||||
assert.NotNil(t, code)
|
||||
|
||||
code, _ = final.Compile(&build.Build{Arch: build.X86})
|
||||
code, _, _ = final.Compile(&build.Build{Arch: build.X86})
|
||||
assert.NotNil(t, code)
|
||||
}
|
|
@ -4,10 +4,21 @@ import "git.urbach.dev/cli/q/src/cpu"
|
|||
|
||||
type Instruction interface{}
|
||||
|
||||
type AndRegisterNumber struct {
|
||||
Destination cpu.Register
|
||||
Source cpu.Register
|
||||
Number int
|
||||
}
|
||||
|
||||
type Call struct {
|
||||
Label string
|
||||
}
|
||||
|
||||
type CallExtern struct {
|
||||
Library string
|
||||
Function string
|
||||
}
|
||||
|
||||
type FunctionStart struct{}
|
||||
type FunctionEnd struct{}
|
||||
|
||||
|
@ -35,4 +46,11 @@ type MoveRegisterRegister struct {
|
|||
}
|
||||
|
||||
type Return struct{}
|
||||
|
||||
type SubRegisterNumber struct {
|
||||
Destination cpu.Register
|
||||
Source cpu.Register
|
||||
Number int
|
||||
}
|
||||
|
||||
type Syscall struct{}
|
|
@ -1,11 +1,15 @@
|
|||
package asm
|
||||
|
||||
import "git.urbach.dev/cli/q/src/dll"
|
||||
|
||||
type compiler struct {
|
||||
code []byte
|
||||
data []byte
|
||||
dataLabels map[string]Address
|
||||
labels map[string]Address
|
||||
deferred []func()
|
||||
code []byte
|
||||
data []byte
|
||||
dataLabels map[string]Address
|
||||
labels map[string]Address
|
||||
libraries dll.List
|
||||
importsStart int
|
||||
deferred []func()
|
||||
}
|
||||
|
||||
func (c *compiler) Defer(call func()) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package asm
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"git.urbach.dev/cli/q/src/sizeof"
|
||||
"git.urbach.dev/cli/q/src/x86"
|
||||
|
@ -13,6 +14,18 @@ type compilerX86 struct {
|
|||
|
||||
func (c *compilerX86) Compile(instr Instruction) {
|
||||
switch instr := instr.(type) {
|
||||
case *AndRegisterNumber:
|
||||
if instr.Destination != instr.Source {
|
||||
c.code = x86.MoveRegisterRegister(c.code, instr.Destination, instr.Source)
|
||||
}
|
||||
|
||||
c.code = x86.AndRegisterNumber(c.code, instr.Destination, instr.Number)
|
||||
case *SubRegisterNumber:
|
||||
if instr.Destination != instr.Source {
|
||||
c.code = x86.MoveRegisterRegister(c.code, instr.Destination, instr.Source)
|
||||
}
|
||||
|
||||
c.code = x86.SubRegisterNumber(c.code, instr.Destination, instr.Number)
|
||||
case *Call:
|
||||
c.code = x86.Call(c.code, 0)
|
||||
end := len(c.code)
|
||||
|
@ -24,6 +37,21 @@ func (c *compilerX86) Compile(instr Instruction) {
|
|||
panic("unknown label: " + instr.Label)
|
||||
}
|
||||
|
||||
offset := address - end
|
||||
binary.LittleEndian.PutUint32(c.code[end-4:end], uint32(offset))
|
||||
})
|
||||
case *CallExtern:
|
||||
c.code = x86.CallAt(c.code, 0)
|
||||
end := len(c.code)
|
||||
|
||||
c.Defer(func() {
|
||||
index := c.libraries.Index(instr.Library, instr.Function)
|
||||
|
||||
if index == -1 {
|
||||
panic(fmt.Sprintf("unknown extern function '%s' in library '%s'", instr.Function, instr.Library))
|
||||
}
|
||||
|
||||
address := c.importsStart + index*8
|
||||
offset := address - end
|
||||
binary.LittleEndian.PutUint32(c.code[end-4:end], uint32(offset))
|
||||
})
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"git.urbach.dev/cli/q/src/cpu"
|
||||
"git.urbach.dev/cli/q/src/ssa"
|
||||
"git.urbach.dev/cli/q/src/types"
|
||||
"git.urbach.dev/cli/q/src/x86"
|
||||
)
|
||||
|
||||
// Compile turns a function into machine code.
|
||||
|
@ -72,11 +73,19 @@ func (f *Function) Compile() {
|
|||
for instr := range f.Values {
|
||||
switch instr := instr.(type) {
|
||||
case *ssa.Call:
|
||||
f.mv(instr.Arguments[1:], f.CPU.Call)
|
||||
arg := instr.Arguments[0].(*ssa.Function)
|
||||
fn := f.All.Functions[arg.UniqueName]
|
||||
|
||||
switch arg := instr.Arguments[0].(type) {
|
||||
case *ssa.Function:
|
||||
f.Assembler.Instructions = append(f.Assembler.Instructions, &asm.Call{Label: arg.UniqueName})
|
||||
if fn.IsExtern() {
|
||||
f.mv(instr.Arguments[1:], f.CPU.ExternCall)
|
||||
f.Assembler.Append(&asm.MoveRegisterRegister{Destination: x86.R5, Source: x86.SP})
|
||||
f.Assembler.Append(&asm.AndRegisterNumber{Destination: x86.SP, Source: x86.SP, Number: -16})
|
||||
f.Assembler.Append(&asm.SubRegisterNumber{Destination: x86.SP, Source: x86.SP, Number: 32})
|
||||
f.Assembler.Append(&asm.CallExtern{Library: fn.Package, Function: fn.Name})
|
||||
f.Assembler.Append(&asm.MoveRegisterRegister{Destination: x86.SP, Source: x86.R5})
|
||||
} else {
|
||||
f.mv(instr.Arguments[1:], f.CPU.Call)
|
||||
f.Assembler.Append(&asm.Call{Label: fn.UniqueName})
|
||||
}
|
||||
|
||||
case *ssa.Syscall:
|
||||
|
|
|
@ -137,7 +137,12 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) {
|
|||
return nil, errors.New(&UnknownIdentifier{Name: label}, f.File, left.Token.Position)
|
||||
}
|
||||
|
||||
f.Dependencies.Add(function)
|
||||
if function.IsExtern() {
|
||||
f.Assembler.Libraries = f.Assembler.Libraries.Append(function.Package, function.Name)
|
||||
} else {
|
||||
f.Dependencies.Add(function)
|
||||
}
|
||||
|
||||
v := f.AppendFunction(function.UniqueName, function.Type)
|
||||
v.Source = ssa.Source(expr.Source)
|
||||
return v, nil
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
type Function struct {
|
||||
ssa.IR
|
||||
Name string
|
||||
Package string
|
||||
UniqueName string
|
||||
File *fs.File
|
||||
Input []*ssa.Parameter
|
||||
|
@ -31,11 +32,12 @@ type Function struct {
|
|||
}
|
||||
|
||||
// NewFunction creates a new function.
|
||||
func NewFunction(name string, file *fs.File) *Function {
|
||||
func NewFunction(name string, pkg string, file *fs.File) *Function {
|
||||
return &Function{
|
||||
Name: name,
|
||||
Package: pkg,
|
||||
UniqueName: fmt.Sprintf("%s.%s", pkg, name),
|
||||
File: file,
|
||||
UniqueName: fmt.Sprintf("%s.%s", file.Package, name),
|
||||
Identifiers: make(map[string]ssa.Value, 8),
|
||||
IR: ssa.IR{
|
||||
Blocks: []*ssa.Block{
|
||||
|
|
|
@ -2,6 +2,7 @@ package cpu
|
|||
|
||||
// CPU represents the processor.
|
||||
type CPU struct {
|
||||
Call []Register
|
||||
Syscall []Register
|
||||
Call []Register
|
||||
Syscall []Register
|
||||
ExternCall []Register
|
||||
}
|
6
src/dll/Library.go
Normal file
6
src/dll/Library.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package dll
|
||||
|
||||
type Library struct {
|
||||
Name string
|
||||
Functions []string
|
||||
}
|
55
src/dll/List.go
Normal file
55
src/dll/List.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package dll
|
||||
|
||||
import "slices"
|
||||
|
||||
// List is a slice of DLLs.
|
||||
type List []Library
|
||||
|
||||
// Append adds a function for the given DLL if it doesn't exist yet.
|
||||
func (list List) Append(dllName string, funcName string) List {
|
||||
for i, dll := range list {
|
||||
if dll.Name != dllName {
|
||||
continue
|
||||
}
|
||||
|
||||
if slices.Contains(dll.Functions, funcName) {
|
||||
return list
|
||||
}
|
||||
|
||||
list[i].Functions = append(list[i].Functions, funcName)
|
||||
return list
|
||||
}
|
||||
|
||||
return append(list, Library{Name: dllName, Functions: []string{funcName}})
|
||||
}
|
||||
|
||||
// Contains returns true if the library exists.
|
||||
func (list List) Contains(dllName string) bool {
|
||||
for _, dll := range list {
|
||||
if dll.Name == dllName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Index returns the position of the given function name.
|
||||
func (list List) Index(dllName string, funcName string) int {
|
||||
index := 0
|
||||
|
||||
for _, dll := range list {
|
||||
if dll.Name != dllName {
|
||||
index += len(dll.Functions) + 1
|
||||
continue
|
||||
}
|
||||
|
||||
for i, fn := range dll.Functions {
|
||||
if fn == funcName {
|
||||
return index + i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
34
src/dll/List_test.go
Normal file
34
src/dll/List_test.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package dll_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.urbach.dev/cli/q/src/dll"
|
||||
"git.urbach.dev/go/assert"
|
||||
)
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
libs := dll.List{}
|
||||
assert.False(t, libs.Contains("kernel32"))
|
||||
assert.Equal(t, -1, libs.Index("kernel32", "ExitProcess"))
|
||||
|
||||
libs = libs.Append("kernel32", "ExitProcess")
|
||||
assert.True(t, libs.Contains("kernel32"))
|
||||
assert.Equal(t, 0, libs.Index("kernel32", "ExitProcess"))
|
||||
|
||||
libs = libs.Append("user32", "MessageBox")
|
||||
assert.True(t, libs.Contains("kernel32"))
|
||||
assert.True(t, libs.Contains("user32"))
|
||||
assert.Equal(t, 0, libs.Index("kernel32", "ExitProcess"))
|
||||
assert.Equal(t, 2, libs.Index("user32", "MessageBox"))
|
||||
|
||||
libs = libs.Append("kernel32", "SetConsoleCP")
|
||||
assert.Equal(t, 0, libs.Index("kernel32", "ExitProcess"))
|
||||
assert.Equal(t, 1, libs.Index("kernel32", "SetConsoleCP"))
|
||||
assert.Equal(t, 3, libs.Index("user32", "MessageBox"))
|
||||
|
||||
libs = libs.Append("kernel32", "ExitProcess")
|
||||
assert.Equal(t, 0, libs.Index("kernel32", "ExitProcess"))
|
||||
assert.Equal(t, 1, libs.Index("kernel32", "SetConsoleCP"))
|
||||
assert.Equal(t, 3, libs.Index("user32", "MessageBox"))
|
||||
}
|
|
@ -9,6 +9,7 @@ import (
|
|||
"git.urbach.dev/cli/q/src/data"
|
||||
"git.urbach.dev/cli/q/src/elf"
|
||||
"git.urbach.dev/cli/q/src/macho"
|
||||
"git.urbach.dev/cli/q/src/pe"
|
||||
)
|
||||
|
||||
// WriteFile writes an executable file to disk.
|
||||
|
@ -33,13 +34,15 @@ func WriteFile(executable string, b *build.Build, env *core.Environment) error {
|
|||
final.Merge(&f.Assembler)
|
||||
})
|
||||
|
||||
code, data := final.Compile(b)
|
||||
code, data, libs := final.Compile(b)
|
||||
|
||||
switch b.OS {
|
||||
case build.Linux:
|
||||
elf.Write(file, b, code, data)
|
||||
case build.Mac:
|
||||
macho.Write(file, b, code, data)
|
||||
case build.Windows:
|
||||
pe.Write(file, b, code, data, libs)
|
||||
}
|
||||
|
||||
err = file.Close()
|
||||
|
|
23
src/pe/Arch.go
Normal file
23
src/pe/Arch.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package pe
|
||||
|
||||
import (
|
||||
"git.urbach.dev/cli/q/src/build"
|
||||
)
|
||||
|
||||
const (
|
||||
IMAGE_FILE_MACHINE_AMD64 = 0x8664
|
||||
IMAGE_FILE_MACHINE_ARM64 = 0xAA64
|
||||
IMAGE_FILE_MACHINE_RISCV64 = 0x5064
|
||||
)
|
||||
|
||||
// Arch returns the CPU architecture used in the PE header.
|
||||
func Arch(arch build.Arch) uint16 {
|
||||
switch arch {
|
||||
case build.ARM:
|
||||
return IMAGE_FILE_MACHINE_ARM64
|
||||
case build.X86:
|
||||
return IMAGE_FILE_MACHINE_AMD64
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
21
src/pe/Characteristics.go
Normal file
21
src/pe/Characteristics.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package pe
|
||||
|
||||
type Characteristics uint16
|
||||
|
||||
const (
|
||||
IMAGE_FILE_RELOCS_STRIPPED Characteristics = 0x0001
|
||||
IMAGE_FILE_EXECUTABLE_IMAGE Characteristics = 0x0002
|
||||
IMAGE_FILE_LINE_NUMS_STRIPPED Characteristics = 0x0004
|
||||
IMAGE_FILE_LOCAL_SYMS_STRIPPED Characteristics = 0x0008
|
||||
IMAGE_FILE_AGGRESSIVE_WS_TRIM Characteristics = 0x0010
|
||||
IMAGE_FILE_LARGE_ADDRESS_AWARE Characteristics = 0x0020
|
||||
IMAGE_FILE_BYTES_REVERSED_LO Characteristics = 0x0080
|
||||
IMAGE_FILE_32BIT_MACHINE Characteristics = 0x0100
|
||||
IMAGE_FILE_DEBUG_STRIPPED Characteristics = 0x0200
|
||||
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP Characteristics = 0x0400
|
||||
IMAGE_FILE_NET_RUN_FROM_SWAP Characteristics = 0x0800
|
||||
IMAGE_FILE_SYSTEM Characteristics = 0x1000
|
||||
IMAGE_FILE_DLL Characteristics = 0x2000
|
||||
IMAGE_FILE_UP_SYSTEM_ONLY Characteristics = 0x4000
|
||||
IMAGE_FILE_BYTES_REVERSED_HI Characteristics = 0x8000
|
||||
)
|
7
src/pe/Constants.go
Normal file
7
src/pe/Constants.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package pe
|
||||
|
||||
const (
|
||||
BaseAddress = 0x400000
|
||||
NumSections = 3
|
||||
HeaderEnd = DOSHeaderSize + NTHeaderSize + OptionalHeader64Size + SectionHeaderSize*NumSections
|
||||
)
|
11
src/pe/DLLImport.go
Normal file
11
src/pe/DLLImport.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package pe
|
||||
|
||||
const DLLImportSize = 20
|
||||
|
||||
type DLLImport struct {
|
||||
RvaFunctionNameList uint32
|
||||
TimeDateStamp uint32
|
||||
ForwarderChain uint32
|
||||
RvaModuleName uint32
|
||||
RvaFunctionAddressList uint32
|
||||
}
|
11
src/pe/DOSHeader.go
Normal file
11
src/pe/DOSHeader.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package pe
|
||||
|
||||
const DOSHeaderSize = 64
|
||||
|
||||
// DOSHeader is at the beginning of each EXE file and nowadays just points
|
||||
// to the PEHeader using an absolute file offset.
|
||||
type DOSHeader struct {
|
||||
Magic [4]byte
|
||||
_ [56]byte
|
||||
NTHeaderOffset uint32
|
||||
}
|
6
src/pe/DataDirectory.go
Normal file
6
src/pe/DataDirectory.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package pe
|
||||
|
||||
type DataDirectory struct {
|
||||
VirtualAddress uint32
|
||||
Size uint32
|
||||
}
|
17
src/pe/DllCharacteristics.go
Normal file
17
src/pe/DllCharacteristics.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package pe
|
||||
|
||||
type DllCharacteristics uint16
|
||||
|
||||
const (
|
||||
IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA DllCharacteristics = 0x0020
|
||||
IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE DllCharacteristics = 0x0040
|
||||
IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY DllCharacteristics = 0x0080
|
||||
IMAGE_DLLCHARACTERISTICS_NX_COMPAT DllCharacteristics = 0x0100
|
||||
IMAGE_DLLCHARACTERISTICS_NO_ISOLATION DllCharacteristics = 0x0200
|
||||
IMAGE_DLLCHARACTERISTICS_NO_SEH DllCharacteristics = 0x0400
|
||||
IMAGE_DLLCHARACTERISTICS_NO_BIND DllCharacteristics = 0x0800
|
||||
IMAGE_DLLCHARACTERISTICS_APPCONTAINER DllCharacteristics = 0x1000
|
||||
IMAGE_DLLCHARACTERISTICS_WDM_DRIVER DllCharacteristics = 0x2000
|
||||
IMAGE_DLLCHARACTERISTICS_GUARD_CF DllCharacteristics = 0x4000
|
||||
IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE DllCharacteristics = 0x8000
|
||||
)
|
147
src/pe/EXE.go
Normal file
147
src/pe/EXE.go
Normal file
|
@ -0,0 +1,147 @@
|
|||
package pe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"git.urbach.dev/cli/q/src/build"
|
||||
"git.urbach.dev/cli/q/src/dll"
|
||||
"git.urbach.dev/cli/q/src/exe"
|
||||
)
|
||||
|
||||
// EXE is the portable executable format used on Windows.
|
||||
type EXE struct {
|
||||
DOSHeader
|
||||
NTHeader
|
||||
OptionalHeader64
|
||||
Sections []SectionHeader
|
||||
}
|
||||
|
||||
// Write writes the EXE file to the given writer.
|
||||
func Write(writer io.WriteSeeker, b *build.Build, codeBytes []byte, dataBytes []byte, libs dll.List) {
|
||||
x := exe.New(HeaderEnd, b.FileAlign, b.MemoryAlign)
|
||||
x.InitSections(codeBytes, dataBytes, nil)
|
||||
code := x.Sections[0]
|
||||
data := x.Sections[1]
|
||||
imports := x.Sections[2]
|
||||
subSystem := IMAGE_SUBSYSTEM_WINDOWS_CUI
|
||||
arch := Arch(b.Arch)
|
||||
|
||||
importsData, dllData, dllImports, dllDataStart := importLibraries(libs, imports.FileOffset)
|
||||
buffer := bytes.Buffer{}
|
||||
binary.Write(&buffer, binary.LittleEndian, &importsData)
|
||||
binary.Write(&buffer, binary.LittleEndian, &dllData)
|
||||
binary.Write(&buffer, binary.LittleEndian, &dllImports)
|
||||
imports.Bytes = buffer.Bytes()
|
||||
importDirectoryStart := dllDataStart + len(dllData)
|
||||
importDirectorySize := DLLImportSize * len(dllImports)
|
||||
imageSize := exe.Align(imports.MemoryOffset+len(imports.Bytes), b.MemoryAlign)
|
||||
|
||||
if libs.Contains("user32") {
|
||||
subSystem = IMAGE_SUBSYSTEM_WINDOWS_GUI
|
||||
}
|
||||
|
||||
pe := &EXE{
|
||||
DOSHeader: DOSHeader{
|
||||
Magic: [4]byte{'M', 'Z', 0, 0},
|
||||
NTHeaderOffset: DOSHeaderSize,
|
||||
},
|
||||
NTHeader: NTHeader{
|
||||
Signature: [4]byte{'P', 'E', 0, 0},
|
||||
Machine: arch,
|
||||
NumberOfSections: uint16(NumSections),
|
||||
TimeDateStamp: 0,
|
||||
PointerToSymbolTable: 0,
|
||||
NumberOfSymbols: 0,
|
||||
SizeOfOptionalHeader: OptionalHeader64Size,
|
||||
Characteristics: IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_LARGE_ADDRESS_AWARE,
|
||||
},
|
||||
OptionalHeader64: OptionalHeader64{
|
||||
Magic: 0x020B, // PE32+ / 64-bit executable
|
||||
MajorLinkerVersion: 0x0E,
|
||||
MinorLinkerVersion: 0x16,
|
||||
SizeOfCode: uint32(len(code.Bytes)),
|
||||
SizeOfInitializedData: 0,
|
||||
SizeOfUninitializedData: 0,
|
||||
AddressOfEntryPoint: uint32(code.MemoryOffset),
|
||||
BaseOfCode: uint32(code.MemoryOffset),
|
||||
ImageBase: BaseAddress,
|
||||
SectionAlignment: uint32(b.MemoryAlign), // power of 2, must be greater than or equal to FileAlignment
|
||||
FileAlignment: uint32(b.FileAlign), // power of 2
|
||||
MajorOperatingSystemVersion: 0x06,
|
||||
MinorOperatingSystemVersion: 0,
|
||||
MajorImageVersion: 0,
|
||||
MinorImageVersion: 0,
|
||||
MajorSubsystemVersion: 0x06,
|
||||
MinorSubsystemVersion: 0,
|
||||
Win32VersionValue: 0,
|
||||
SizeOfImage: uint32(imageSize), // a multiple of SectionAlignment
|
||||
SizeOfHeaders: uint32(code.FileOffset), // section bodies begin here
|
||||
CheckSum: 0,
|
||||
Subsystem: subSystem,
|
||||
DllCharacteristics: IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA | IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE | IMAGE_DLLCHARACTERISTICS_NX_COMPAT | IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE,
|
||||
SizeOfStackReserve: 0x100000,
|
||||
SizeOfStackCommit: 0x1000,
|
||||
SizeOfHeapReserve: 0x100000,
|
||||
SizeOfHeapCommit: 0x1000,
|
||||
LoaderFlags: 0,
|
||||
NumberOfRvaAndSizes: 16,
|
||||
DataDirectory: [16]DataDirectory{
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
{VirtualAddress: uint32(importDirectoryStart), Size: uint32(importDirectorySize)}, // RVA of the imported function table
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
{VirtualAddress: uint32(imports.MemoryOffset), Size: uint32(len(importsData) * 8)}, // RVA of the import address table
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
{VirtualAddress: 0, Size: 0},
|
||||
},
|
||||
},
|
||||
Sections: []SectionHeader{
|
||||
{
|
||||
Name: [8]byte{'.', 't', 'e', 'x', 't'},
|
||||
VirtualSize: uint32(len(code.Bytes)),
|
||||
VirtualAddress: uint32(code.MemoryOffset),
|
||||
RawSize: uint32(len(code.Bytes)), // must be a multiple of FileAlignment
|
||||
RawAddress: uint32(code.FileOffset), // must be a multiple of FileAlignment
|
||||
Characteristics: IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ,
|
||||
},
|
||||
{
|
||||
Name: [8]byte{'.', 'r', 'd', 'a', 't', 'a'},
|
||||
VirtualSize: uint32(len(data.Bytes)),
|
||||
VirtualAddress: uint32(data.MemoryOffset),
|
||||
RawSize: uint32(len(data.Bytes)),
|
||||
RawAddress: uint32(data.FileOffset),
|
||||
Characteristics: IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ,
|
||||
},
|
||||
{
|
||||
Name: [8]byte{'.', 'i', 'd', 'a', 't', 'a'},
|
||||
VirtualSize: uint32(len(imports.Bytes)),
|
||||
VirtualAddress: uint32(imports.MemoryOffset),
|
||||
RawSize: uint32(len(imports.Bytes)),
|
||||
RawAddress: uint32(imports.FileOffset),
|
||||
Characteristics: IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
binary.Write(writer, binary.LittleEndian, &pe.DOSHeader)
|
||||
binary.Write(writer, binary.LittleEndian, &pe.NTHeader)
|
||||
binary.Write(writer, binary.LittleEndian, &pe.OptionalHeader64)
|
||||
binary.Write(writer, binary.LittleEndian, &pe.Sections)
|
||||
writer.Seek(int64(code.Padding), io.SeekCurrent)
|
||||
writer.Write(code.Bytes)
|
||||
writer.Seek(int64(data.Padding), io.SeekCurrent)
|
||||
writer.Write(data.Bytes)
|
||||
writer.Seek(int64(imports.Padding), io.SeekCurrent)
|
||||
writer.Write(imports.Bytes)
|
||||
}
|
15
src/pe/EXE_test.go
Normal file
15
src/pe/EXE_test.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package pe_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.urbach.dev/cli/q/src/build"
|
||||
"git.urbach.dev/cli/q/src/exe"
|
||||
"git.urbach.dev/cli/q/src/pe"
|
||||
)
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
pe.Write(&exe.Discard{}, &build.Build{Arch: build.ARM, FileAlign: 0x4000, MemoryAlign: 0x4000}, nil, nil, nil)
|
||||
pe.Write(&exe.Discard{}, &build.Build{Arch: build.X86, FileAlign: 0x1000, MemoryAlign: 0x1000}, nil, nil, nil)
|
||||
pe.Write(&exe.Discard{}, &build.Build{Arch: build.UnknownArch, FileAlign: 0x1000, MemoryAlign: 0x1000}, nil, nil, nil)
|
||||
}
|
14
src/pe/NTHeader.go
Normal file
14
src/pe/NTHeader.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package pe
|
||||
|
||||
const NTHeaderSize = 24
|
||||
|
||||
type NTHeader struct {
|
||||
Signature [4]byte
|
||||
Machine uint16
|
||||
NumberOfSections uint16
|
||||
TimeDateStamp uint32
|
||||
PointerToSymbolTable uint32
|
||||
NumberOfSymbols uint32
|
||||
SizeOfOptionalHeader uint16
|
||||
Characteristics Characteristics
|
||||
}
|
36
src/pe/OptionalHeader64.go
Normal file
36
src/pe/OptionalHeader64.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package pe
|
||||
|
||||
const OptionalHeader64Size = 240
|
||||
|
||||
type OptionalHeader64 struct {
|
||||
Magic uint16
|
||||
MajorLinkerVersion uint8
|
||||
MinorLinkerVersion uint8
|
||||
SizeOfCode uint32
|
||||
SizeOfInitializedData uint32
|
||||
SizeOfUninitializedData uint32
|
||||
AddressOfEntryPoint uint32
|
||||
BaseOfCode uint32
|
||||
ImageBase uint64
|
||||
SectionAlignment uint32
|
||||
FileAlignment uint32
|
||||
MajorOperatingSystemVersion uint16
|
||||
MinorOperatingSystemVersion uint16
|
||||
MajorImageVersion uint16
|
||||
MinorImageVersion uint16
|
||||
MajorSubsystemVersion uint16
|
||||
MinorSubsystemVersion uint16
|
||||
Win32VersionValue uint32
|
||||
SizeOfImage uint32
|
||||
SizeOfHeaders uint32
|
||||
CheckSum uint32
|
||||
Subsystem Subsystem
|
||||
DllCharacteristics DllCharacteristics
|
||||
SizeOfStackReserve uint64
|
||||
SizeOfStackCommit uint64
|
||||
SizeOfHeapReserve uint64
|
||||
SizeOfHeapCommit uint64
|
||||
LoaderFlags uint32
|
||||
NumberOfRvaAndSizes uint32
|
||||
DataDirectory [16]DataDirectory
|
||||
}
|
14
src/pe/SectionCharacteristics.go
Normal file
14
src/pe/SectionCharacteristics.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package pe
|
||||
|
||||
type SectionCharacteristics uint32
|
||||
|
||||
const (
|
||||
IMAGE_SCN_CNT_CODE SectionCharacteristics = 0x00000020
|
||||
IMAGE_SCN_CNT_INITIALIZED_DATA SectionCharacteristics = 0x00000040
|
||||
IMAGE_SCN_CNT_UNINITIALIZED_DATA SectionCharacteristics = 0x00000080
|
||||
IMAGE_SCN_LNK_COMDAT SectionCharacteristics = 0x00001000
|
||||
IMAGE_SCN_MEM_DISCARDABLE SectionCharacteristics = 0x02000000
|
||||
IMAGE_SCN_MEM_EXECUTE SectionCharacteristics = 0x20000000
|
||||
IMAGE_SCN_MEM_READ SectionCharacteristics = 0x40000000
|
||||
IMAGE_SCN_MEM_WRITE SectionCharacteristics = 0x80000000
|
||||
)
|
16
src/pe/SectionHeader.go
Normal file
16
src/pe/SectionHeader.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package pe
|
||||
|
||||
const SectionHeaderSize = 40
|
||||
|
||||
type SectionHeader struct {
|
||||
Name [8]byte
|
||||
VirtualSize uint32
|
||||
VirtualAddress uint32
|
||||
RawSize uint32
|
||||
RawAddress uint32
|
||||
PointerToRelocations uint32
|
||||
PointerToLineNumbers uint32
|
||||
NumberOfRelocations uint16
|
||||
NumberOfLineNumbers uint16
|
||||
Characteristics SectionCharacteristics
|
||||
}
|
20
src/pe/Subsystem.go
Normal file
20
src/pe/Subsystem.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package pe
|
||||
|
||||
type Subsystem uint16
|
||||
|
||||
const (
|
||||
IMAGE_SUBSYSTEM_UNKNOWN Subsystem = 0
|
||||
IMAGE_SUBSYSTEM_NATIVE Subsystem = 1
|
||||
IMAGE_SUBSYSTEM_WINDOWS_GUI Subsystem = 2
|
||||
IMAGE_SUBSYSTEM_WINDOWS_CUI Subsystem = 3
|
||||
IMAGE_SUBSYSTEM_OS2_CUI Subsystem = 5
|
||||
IMAGE_SUBSYSTEM_POSIX_CUI Subsystem = 7
|
||||
IMAGE_SUBSYSTEM_NATIVE_WINDOWS Subsystem = 8
|
||||
IMAGE_SUBSYSTEM_WINDOWS_CE_GUI Subsystem = 9
|
||||
IMAGE_SUBSYSTEM_EFI_APPLICATION Subsystem = 10
|
||||
IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER Subsystem = 11
|
||||
IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER Subsystem = 12
|
||||
IMAGE_SUBSYSTEM_EFI_ROM Subsystem = 13
|
||||
IMAGE_SUBSYSTEM_XBOX Subsystem = 14
|
||||
IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION Subsystem = 16
|
||||
)
|
60
src/pe/importLibraries.go
Normal file
60
src/pe/importLibraries.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package pe
|
||||
|
||||
import "git.urbach.dev/cli/q/src/dll"
|
||||
|
||||
// importLibraries generates the import address table which contains the addresses of functions imported from DLLs.
|
||||
func importLibraries(dlls dll.List, importsStart int) ([]uint64, []byte, []DLLImport, int) {
|
||||
imports := make([]uint64, 0)
|
||||
dllData := make([]byte, 0)
|
||||
dllImports := []DLLImport{}
|
||||
|
||||
for _, library := range dlls {
|
||||
functionsStart := len(imports) * 8
|
||||
dllNamePos := len(dllData)
|
||||
dllData = append(dllData, library.Name...)
|
||||
dllData = append(dllData, ".dll"...)
|
||||
dllData = append(dllData, 0x00)
|
||||
|
||||
dllImports = append(dllImports, DLLImport{
|
||||
RvaFunctionNameList: uint32(importsStart + functionsStart),
|
||||
TimeDateStamp: 0,
|
||||
ForwarderChain: 0,
|
||||
RvaModuleName: uint32(dllNamePos),
|
||||
RvaFunctionAddressList: uint32(importsStart + functionsStart),
|
||||
})
|
||||
|
||||
for _, fn := range library.Functions {
|
||||
if len(dllData)&1 != 0 {
|
||||
dllData = append(dllData, 0x00) // align the next entry on an even boundary
|
||||
}
|
||||
|
||||
offset := len(dllData)
|
||||
dllData = append(dllData, 0x00, 0x00)
|
||||
dllData = append(dllData, fn...)
|
||||
dllData = append(dllData, 0x00)
|
||||
|
||||
imports = append(imports, uint64(offset))
|
||||
}
|
||||
|
||||
imports = append(imports, 0)
|
||||
}
|
||||
|
||||
dllDataStart := importsStart + len(imports)*8
|
||||
|
||||
for i := range imports {
|
||||
if imports[i] == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
imports[i] += uint64(dllDataStart)
|
||||
}
|
||||
|
||||
for i := range dllImports {
|
||||
dllImports[i].RvaModuleName += uint32(dllDataStart)
|
||||
}
|
||||
|
||||
// a zeroed structure marks the end of the list
|
||||
dllImports = append(dllImports, DLLImport{})
|
||||
|
||||
return imports, dllData, dllImports, dllDataStart
|
||||
}
|
68
src/scanner/scanExtern.go
Normal file
68
src/scanner/scanExtern.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package scanner
|
||||
|
||||
import (
|
||||
"git.urbach.dev/cli/q/src/errors"
|
||||
"git.urbach.dev/cli/q/src/fs"
|
||||
"git.urbach.dev/cli/q/src/token"
|
||||
)
|
||||
|
||||
// scanExtern scans a block of external libraries.
|
||||
func (s *scanner) scanExtern(file *fs.File, tokens token.List, i int) (int, error) {
|
||||
i++
|
||||
|
||||
if tokens[i].Kind != token.BlockStart {
|
||||
return i, errors.New(MissingBlockStart, file, tokens[i].Position)
|
||||
}
|
||||
|
||||
for i < len(tokens) {
|
||||
switch tokens[i].Kind {
|
||||
case token.Identifier:
|
||||
var err error
|
||||
i, err = s.scanExternLibrary(file, tokens, i)
|
||||
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
|
||||
case token.BlockEnd:
|
||||
return i, nil
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
return i, errors.New(MissingBlockEnd, file, tokens[i].Position)
|
||||
}
|
||||
|
||||
// scanExternLibrary scans a block of external function declarations.
|
||||
func (s *scanner) scanExternLibrary(file *fs.File, tokens token.List, i int) (int, error) {
|
||||
dllName := tokens[i].String(file.Bytes)
|
||||
i++
|
||||
|
||||
if tokens[i].Kind != token.BlockStart {
|
||||
return i, errors.New(MissingBlockStart, file, tokens[i].Position)
|
||||
}
|
||||
|
||||
i++
|
||||
|
||||
for i < len(tokens) {
|
||||
switch tokens[i].Kind {
|
||||
case token.Identifier:
|
||||
function, j, err := scanSignature(file, dllName, tokens, i, token.NewLine)
|
||||
|
||||
if err != nil {
|
||||
return j, err
|
||||
}
|
||||
|
||||
i = j
|
||||
s.functions <- function
|
||||
|
||||
case token.BlockEnd:
|
||||
return i, nil
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
return i, errors.New(MissingBlockEnd, file, tokens[i].Position)
|
||||
}
|
|
@ -33,6 +33,8 @@ func (s *scanner) scanFile(path string, pkg string) error {
|
|||
case token.Comment:
|
||||
case token.Identifier:
|
||||
i, err = s.scanFunction(file, tokens, i)
|
||||
case token.Extern:
|
||||
i, err = s.scanExtern(file, tokens, i)
|
||||
case token.Import:
|
||||
i, err = s.scanImport(file, tokens, i)
|
||||
case token.EOF:
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
|
||||
// scanFunction scans a function.
|
||||
func (s *scanner) scanFunction(file *fs.File, tokens token.List, i int) (int, error) {
|
||||
function, i, err := scanSignature(file, tokens, i, token.BlockStart)
|
||||
function, i, err := scanSignature(file, file.Package, tokens, i, token.BlockStart)
|
||||
|
||||
if err != nil {
|
||||
return i, err
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
)
|
||||
|
||||
// scanSignature scans only the function signature without the body.
|
||||
func scanSignature(file *fs.File, tokens token.List, i int, delimiter token.Kind) (*core.Function, int, error) {
|
||||
func scanSignature(file *fs.File, pkg string, tokens token.List, i int, delimiter token.Kind) (*core.Function, int, error) {
|
||||
var (
|
||||
groupLevel = 0
|
||||
nameStart = i
|
||||
|
@ -83,7 +83,7 @@ func scanSignature(file *fs.File, tokens token.List, i int, delimiter token.Kind
|
|||
}
|
||||
|
||||
name := tokens[nameStart].String(file.Bytes)
|
||||
function := core.NewFunction(name, file)
|
||||
function := core.NewFunction(name, pkg, file)
|
||||
parameters := tokens[inputStart:inputEnd]
|
||||
|
||||
for param := range parameters.Split {
|
||||
|
|
|
@ -22,6 +22,7 @@ const (
|
|||
)
|
||||
|
||||
var CPU = cpu.CPU{
|
||||
Call: []cpu.Register{R0, R7, R6, R2, R10, R8, R9},
|
||||
Syscall: []cpu.Register{R0, R7, R6, R2, R10, R8, R9},
|
||||
Call: []cpu.Register{R0, R7, R6, R2, R10, R8, R9},
|
||||
Syscall: []cpu.Register{R0, R7, R6, R2, R10, R8, R9},
|
||||
ExternCall: []cpu.Register{R1, R2, R8, R9},
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue