diff --git a/e2e_test.go b/e2e_test.go index ae446a1..8a5bbc4 100644 --- a/e2e_test.go +++ b/e2e_test.go @@ -1,8 +1,6 @@ package main import ( - "io" - "os" "path/filepath" "runtime" "testing" @@ -157,27 +155,16 @@ func TestE2E(t *testing.T) { for _, tc := range tests { t.Run(tc, func(t *testing.T) { - f, err := os.Open(filepath.Join("./tests/", tc)) - if err != nil { - t.Fatalf("open file: %s, %s", tc, err) - } - defer f.Close() - - buff, err := io.ReadAll(f) - if err != nil { - t.Fatalf("read file: %s, %s", tc, err) - } - - emulator, err := New(buff) + cpu, err := initCPU(filepath.Join("./tests/", tc)) if err != nil { t.Fatalf("initialize RV: %s, %s", tc, err) } - if emulator.tohost == 0 { + if cpu.tohost == 0 { t.Fatalf("unexpected error: tohost is 0 but expected some address in the binary! %s", tc) } - if err := emulator.Start(); err != nil { + if err := cpu.Start(); err != nil { t.Errorf("fail to run: %s, %s", tc, err) } diff --git a/elf.go b/elf.go deleted file mode 100644 index 296d7a9..0000000 --- a/elf.go +++ /dev/null @@ -1,304 +0,0 @@ -package main - -import "fmt" - -// ELFFile is an ELF representation. -type ELFFile struct { - Header *ELFHeader - Sections []*Section - Programs []*Program - - // ToHost is a rv specific information. - // In riscv-tests, tohost is used to tell the emulator that the - // test program ends at the address. - // If ToHost is not 0, the program is likely the test program from riscv-tests. - // https://riscv.org/wp-content/uploads/2015/01/riscv-testing-frameworks-bootcamp-jan2015.pdf - ToHost uint64 -} - -type ELFHeader struct { - Class uint8 - Data uint8 - ELFVersion uint8 - OSABI uint8 - ABIVersion uint8 - Type uint16 - Machine uint16 - Version uint32 - Entry uint64 - PhOff uint64 - ShOff uint64 - Flags uint32 - EhSize uint16 - PhEntSize uint16 - PhNum uint16 - ShEntSize uint16 - ShNum uint16 - ShStrNdx uint16 -} - -type Section struct { - Name uint32 - Type uint32 - Flags uint64 - Addr uint64 - Offset uint64 - Size uint64 - Link uint32 - Info uint32 - AddrAlign uint64 - EntSize uint64 -} - -type Program struct { - Type uint32 - Flags uint32 - Offset uint64 - VAddr uint64 - PAddr uint64 - Filesz uint64 - Memsz uint64 - Align uint64 -} - -// LoadELF loads the given ELF data. -// It currently just loads the program in the ELF, do not load any other useful info such as symbols. -// Additionally, it tries to find .tohost address which is a part of RISC-V specification if defined. -func LoadELF(data []uint8) (*ELFFile, error) { - if len(data) < 4 { - return nil, fmt.Errorf("too short, the file seems not to be a valid ELF") - } - - if data[0] != 0x7f || data[1] != 'E' || data[2] != 'L' || data[3] != 'F' { - return nil, fmt.Errorf("invalid magic number, the file seems not to be a valid ELF") - } - - f := &ELFFile{ - Header: &ELFHeader{}, - } - - /* - * 1. Load ELFHeader - */ - f.Header.Class = read8(data, 4) - f.Header.Data = read8(data, 5) - f.Header.ELFVersion = read8(data, 6) - f.Header.OSABI = read8(data, 7) - f.Header.ABIVersion = read8(data, 8) - f.Header.Type = read16(data, 16) - f.Header.Machine = read16(data, 18) - f.Header.Version = read32(data, 20) - - offset := uint64(24) - - switch f.Header.Class { - case 1: // 32-bit - f.Header.Entry = uint64(read32(data, offset)) - offset += 4 - f.Header.PhOff = uint64(read32(data, offset)) - offset += 4 - f.Header.ShOff = uint64(read32(data, offset)) - offset += 4 - case 2: // 64-bit - f.Header.Entry = read64(data, offset) - offset += 8 - f.Header.PhOff = read64(data, offset) - offset += 8 - f.Header.ShOff = read64(data, offset) - offset += 8 - } - - f.Header.Flags = read32(data, offset) - offset += 4 - - f.Header.EhSize = read16(data, offset) - offset += 2 - - f.Header.PhEntSize = read16(data, offset) - offset += 2 - f.Header.PhNum = read16(data, offset) - offset += 2 - f.Header.ShEntSize = read16(data, offset) - offset += 2 - f.Header.ShNum = read16(data, offset) - offset += 2 - f.Header.ShStrNdx = read16(data, offset) - - /* - * 2. Load Section Headers - */ - offset = f.Header.ShOff - for i := 0; i < int(f.Header.ShNum); i++ { - s := &Section{} - s.Name = read32(data, offset) - offset += 4 - s.Type = read32(data, offset) - offset += 4 - - switch f.Header.Class { - case 1: // 32-bit - s.Flags = uint64(read32(data, offset)) - offset += 4 - s.Addr = uint64(read32(data, offset)) - offset += 4 - s.Offset = uint64(read32(data, offset)) - offset += 4 - s.Size = uint64(read32(data, offset)) - offset += 4 - case 2: // 64-bit - s.Flags = read64(data, offset) - offset += 8 - s.Addr = read64(data, offset) - offset += 8 - s.Offset = read64(data, offset) - offset += 8 - s.Size = read64(data, offset) - offset += 8 - } - - s.Link = read32(data, offset) - offset += 4 - s.Info = read32(data, offset) - offset += 4 - - switch f.Header.Class { - case 1: // 32-bit - s.AddrAlign = uint64(read32(data, offset)) - offset += 4 - s.EntSize = uint64(read32(data, offset)) - offset += 4 - case 2: // 64-bit - s.AddrAlign = read64(data, offset) - offset += 8 - s.EntSize = read64(data, offset) - offset += 8 - } - - f.Sections = append(f.Sections, s) - } - - /* - * 3. Load Program Headers - */ - offset = f.Header.PhOff - for i := 0; i < int(f.Header.PhNum); i++ { - p := &Program{} - p.Type = read32(data, offset) - offset += 4 - - if f.Header.Class == 2 { - // if 64-bit, must read flags here. - // flags place is not the same in 32-bit and 64-bit. - // https://docs.oracle.com/cd/E19683-01/816-1386/chapter6-83432/index.html - p.Flags = read32(data, offset) - offset += 4 - } - - switch f.Header.Class { - case 1: // 32-bit - p.Offset = uint64(read32(data, offset)) - offset += 4 - p.VAddr = uint64(read32(data, offset)) - offset += 4 - p.PAddr = uint64(read32(data, offset)) - offset += 4 - p.Filesz = uint64(read32(data, offset)) - offset += 4 - p.Memsz = uint64(read32(data, offset)) - offset += 4 - case 2: // 64-bit - p.Offset = read64(data, offset) - offset += 8 - p.VAddr = read64(data, offset) - offset += 8 - p.PAddr = read64(data, offset) - offset += 8 - p.Filesz = read64(data, offset) - offset += 8 - p.Memsz = read64(data, offset) - offset += 8 - } - - if f.Header.Class == 1 { - // if 32-bit, must read flags here. - p.Flags = read32(data, offset) - offset += 4 - } - - switch f.Header.Class { - case 1: // 32-bit - p.Align = uint64(read32(data, offset)) - offset += 4 - case 2: // 64-bit - p.Align = read64(data, offset) - offset += 8 - } - - f.Programs = append(f.Programs, p) - } - - /* - * 4. Try to find ToHost Address - * This is not necessarily required in just loading ELF, - * but ToHost is used when running riscv-tests (https://github.com/riscv-software-src/riscv-tests) code. - */ - progDataSectionHeaders := []*Section{} - stringTableSectionHeaders := []*Section{} - for _, s := range f.Sections { - if s.Type == 1 { - progDataSectionHeaders = append(progDataSectionHeaders, s) - } else if s.Type == 3 { - stringTableSectionHeaders = append(stringTableSectionHeaders, s) - } - } - - tohost := []uint8{0x2e, 0x74, 0x6f, 0x68, 0x6f, 0x73, 0x74, 0x00} // ".tohost\null" - for _, pds := range progDataSectionHeaders { - addr := pds.Addr - name := pds.Name - for _, sts := range stringTableSectionHeaders { - offset := sts.Offset - size := sts.Size - found := true - for i := range tohost { - a := offset + uint64(name) + uint64(i) - if a >= offset+size || read8(data, a) != tohost[i] { - found = false - break - } - } - - if found { - f.ToHost = addr - } - } - } - - return f, nil -} - -func read8(data []uint8, addr uint64) uint8 { - return data[addr] -} - -func read16(data []uint8, addr uint64) uint16 { - return uint16(data[addr]) | uint16(data[addr+1])<<8 -} - -func read32(data []uint8, addr uint64) uint32 { - return uint32(data[addr]) | - uint32(data[addr+1])<<8 | - uint32(data[addr+2])<<16 | - uint32(data[addr+3])<<24 -} - -func read64(data []uint8, addr uint64) uint64 { - return uint64(data[addr]) | - uint64(data[addr+1])<<8 | - uint64(data[addr+2])<<16 | - uint64(data[addr+3])<<24 | - uint64(data[addr+4])<<32 | - uint64(data[addr+5])<<40 | - uint64(data[addr+6])<<48 | - uint64(data[addr+7])<<56 -} diff --git a/main.go b/main.go index 5723ef5..e4caf31 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,9 @@ package main import ( + "debug/elf" "flag" "fmt" - "io" "os" ) @@ -17,53 +17,6 @@ type RV struct { tohost uint64 } -func New(prog []byte) (*RV, error) { - elf, err := LoadELF(prog) - if err != nil { - return nil, fmt.Errorf("load ELF: %w", err) - } - - if elf.Header.Data != 1 { // Little endian - return nil, fmt.Errorf("elf data is not ET_EXEC little endian but %d", elf.Header.Data) - } - - if elf.Header.Type != 2 { // ET_EXEC - return nil, fmt.Errorf("elf type is not ET_EXEC but %d", elf.Header.Type) - } - - if elf.Header.Machine != 0xf3 { // RISC-V - return nil, fmt.Errorf("elf machine is not RISC-V but %d", elf.Header.Machine) - } - - if elf.Header.PhNum == 0 { // assert just in case - return nil, fmt.Errorf("elf contains no program headers") - } - - if elf.Header.Class != 2 { - return nil, fmt.Errorf("only 64 bit is supported") - } - - cpu := NewCPU() - - for _, p := range elf.Programs { - if p.Type != 1 { // PT_LOAD - continue - } - - // write to memory - for i := 0; i < int(p.Filesz); i++ { - addr := p.VAddr + uint64(i) - val := uint64(prog[int(p.Offset)+i]) - cpu.write(addr, val, byt) - } - } - cpu.pc = elf.Header.Entry - - rv := &RV{cpu: cpu, tohost: elf.ToHost} - - return rv, nil -} - func (r *RV) Start() error { for { r.cpu.tick() @@ -111,25 +64,62 @@ func run() error { return fmt.Errorf("program must be passed with -p option") } - f, err := os.Open(file) + cpu, err := initCPU(file) if err != nil { - return fmt.Errorf("open program: %w", err) + return fmt.Errorf("initialize emulator: %w", err) + } + + if err := cpu.Start(); err != nil { + return fmt.Errorf("run program: %w", err) } - defer f.Close() - buff, err := io.ReadAll(f) + return nil +} + +func initCPU(filename string) (*RV, error) { + of, err := os.Open(filename) if err != nil { - return fmt.Errorf("read program: %w", err) + return nil, fmt.Errorf("open elf file: %w", err) } - emulator, err := New(buff) + // load elf file + f, err := elf.Open(filename) if err != nil { - return fmt.Errorf("initialize emulator: %w", err) + return nil, fmt.Errorf("open elf file: %w", err) } - if err := emulator.Start(); err != nil { - return fmt.Errorf("run program: %w", err) + if f.Data != elf.ELFDATA2LSB { + return nil, fmt.Errorf("elf must be little endian") } - return nil + if f.Type != elf.ET_EXEC { + return nil, fmt.Errorf("elf type must be ET_EXEC") + } + + if f.Machine != elf.EM_RISCV { + return nil, fmt.Errorf("elf machine must be RISCV") + } + + cpu := NewCPU() + + for _, p := range f.Progs { + if p.Type != elf.PT_LOAD { + continue + } + + for i := 0; i < int(p.Filesz); i++ { + addr := p.Vaddr + uint64(i) + val := make([]byte, byt) + _, err := of.ReadAt(val, int64(p.Off)+int64(i)) + if err != nil { + return nil, fmt.Errorf("read program header") + } + cpu.ram.Write(addr, uint64(val[0]), byt) + } + } + cpu.pc = f.Entry + + rv := &RV{cpu: cpu, tohost: 0x80001000} // TODO: find tohost from sections + + return rv, nil }