Gavare's eXperimental Emulator: Experimenting with GXemul

Back to the index

## Experimenting with GXemul

- Hello world
- Experimental devices

### Hello world:

You might want to use the emulator to develop programs on your own, not just run precompiled kernels such as NetBSD. To get started, I recommend that you do two things:

- Build and install a cross-compiler for MIPS.
- Compile this hello world program, and run it in the emulator.

    /*  Hello world for GXemul  */

    /*  Note: The cast to a signed int causes the address to be sign-extended
        correctly to 0xffffffffb00000xx when compiled in 64-bit mode  */
    #define	PUTCHAR_ADDRESS		((signed int)0xb0000000)
    #define	HALT_ADDRESS		((signed int)0xb0000010)

    void printchar(char ch)
    {
    	*((volatile unsigned char *) PUTCHAR_ADDRESS) = ch;
    }

    void halt(void)
    {
    	*((volatile unsigned char *) HALT_ADDRESS) = 0;
    }

    void printstr(char *s)
    {
    	while (*s)
    		printchar(*s++);
    }

    void f(void)
    {
    	printstr("Hello world\n");
    	halt();
    }

(This hello world program is available here as well: `hello_mips.c`)

I recommend that you build a GCC cross compiler for the mips64-unknown-elf target, and install it. Other compilers could work too, but GCC is good because of its portability. Then try to compile and link the hello world program:

    	$ mips64-unknown-elf-gcc -O2 hello_mips.c -mips4 -mabi=64 -c
    	$ mips64-unknown-elf-ld -Ttext 0xa800000000030000 -e f hello_mips.o -o hello_mips --oformat=elf64-bigmips
    	$ file hello_mips
    	hello_mips: ELF 64-bit MSB mips-4 executable, MIPS R3000_BE, version 1 (SYSV), statically linked, not stripped
    	$ gxemul -q -E testmips hello_mips
    	Hello world

    	$ mips64-unknown-elf-gcc -O2 hello_mips.c -c
    	$ mips64-unknown-elf-ld -Ttext 0x80030000 -e f hello_mips.o -o hello_mips
    	$ file hello_mips
    	hello_mips: ELF 32-bit MSB mips-3 executable, MIPS R3000_BE, version 1 (SYSV), statically linked, not stripped
    	$ gxemul -q -E testmips hello_mips
    	Hello world

As you can see above, a GCC configured for mips64-unknown-elf can produce both 64-bit and 32-bit binaries. If you don't want to run the entire Hello World program, but want to single-step through the execution to learn more about how MIPS programs run, then add -V to the command line:

    	$ gxemul -V -E testmips hello_mips
    	..
    	GXemul> r
    	cpu0:    pc = a800000000030078    <f>
    	cpu0:    hi = 0000000000000000    lo = 0000000000000000
    	cpu0:    zr = 0000000000000000    at = 0000000000000000
    	cpu0:    v0 = 0000000000000000    v1 = 0000000000000000
    	..
    	cpu0:    gp = a8000000000780c0    sp = ffffffffa0007f00
    	cpu0:    fp = 0000000000000000    ra = 0000000000000000
    	GXemul> s 15
    	<f>
    	a800000000030078: 67bdfff0      daddiu  sp,sp,-16
    	a80000000003007c: 3c04a800      lui     a0,0xa800
    	a800000000030080: 3c010003      lui     at,0x3
    	a800000000030084: 64840000      daddiu  a0,a0,0
    	a800000000030088: 642100b8      daddiu  at,at,184
    	a80000000003008c: 0004203c      dsll32  a0,a0,0
    	a800000000030090: 0081202d      daddu   a0,a0,at
    	a800000000030094: ffbf0000      sd      ra,0(sp)  [0xffffffffa0007ef0, data=0x0000000000000000]
    	a800000000030098: 0c00c00a      jal     0xa800000000030028 <printstr>
    	a80000000003009c: 00000000 (d)  nop
    	  <printstr("Hello world\n",0,0,0,..)>
    	<printstr>
    	a800000000030028: 67bdfff0      daddiu  sp,sp,-16
    	a80000000003002c: ffb00000      sd      s0,0(sp)  [0xffffffffa0007ee0, data=0x0000000000000000]
    	a800000000030030: ffbf0008      sd      ra,8(sp)  [0xffffffffa0007ee8, data=0xa8000000000300a0]
    	a800000000030034: 90820000      lbu     v0,0(a0)  [0xa8000000000300b8 = $LC0, data=0x48]
    	a800000000030038: 00021600      sll     v0,v0,24
    	GXemul> print v0
    	v0 = 0x0000000048000000
    	GXemul> _

The syntax of the single-step debugger shouldn't be too hard to grasp. Type "`s`" to single-step one instruction. For some commands (e.g. the single-step command), just pressing enter on a blank line will cause the last command to be repeated. Type "`quit`" to quit.

Hopefully this is enough to get you inspired. :-)

### Experimental devices:

The emulator has several modes where it doesn't emulate any real machine. It can either run in "bare" mode, where no devices are included by default (just the CPU), or in a "test" mode where some simple devices are emulated.

The test machines (`testmips`, `testppc`, etc) have the following experimental devices:

`cons`:

A simple console device, for writing characters to the controlling terminal and receiving keypresses.

Source code: `src/devices/dev_cons.c`
Default physical address: 0x10000000

Offset: | Effect:
`0x00` | Read: `getchar()` (non-blocking; returns `0` if no char was available) Write: `putchar(ch)`
`0x10` | Read or write: `halt()` (Useful for exiting the emulator.)

`mp`:

This device controls the behaviour of CPUs in an emulated multi-processor system.

Source code: `src/devices/dev_mp.c`
Default physical address: 0x11000000

Offset: | Effect:
`0x0000` | Read: `whoami()`. Returns the id of the CPU doing the read.
`0x0010` | Read: `ncpus()`. Returns the number of CPUs in the system.
`0x0020` | Write: `startupcpu(i)`. Starts CPU i. It begins execution at the address set by a write to startupaddr (see below).
`0x0030` | Write: `startupaddr(addr)`. Sets the starting address for CPUs.
`0x0040` | Write: `pause_addr(addr)`. Sets the pause address. (TODO: This is not used anymore?)
`0x0050` | Write: `pause_cpu(i)`. Stops all CPUs except CPU i.
`0x0060` | Write: `unpause_cpu(i)`. Unpauses all CPUs except CPU i.
`0x0070` | Write: `startupstack(addr)`. Sets the startup stack address. (CPUs started with startupcpu() above will have their stack pointer set to this value.)
`0x0080` | Read: `hardware_random()`. This produces a "random" number.
`0x0090` | Read: `memory()`. Returns the number of bytes of RAM in the system.
`0x00a0` | Write: `ipi_one((nr << 16) + cpuid)`. Sends IPI `nr` to a specific CPU.
`0x00b0` | Write: `ipi_many((nr << 16) + cpuid)`. Sends IPI `nr` to all CPUs except the specified one.
0x00c0 | Read: `ipi_read()`. Returns the next pending IPI. 0 is returned if there is no pending IPI (so 0 shouldn't be used for valid IPIs). Hardware int 6 is deasserted when the IPI queue is empty. Write: `ipi_flush()`. Clears the IPI queue, discarding any pending IPIs.
0x00d0 | Read: `ncycles()`. Returns approximately the number of cycles executed. Note: this value is not updated for every instruction, so it cannot be used for small measurements.

`fb`:

A simple linear framebuffer, for graphics output. 640 x 480 pixels, 3 bytes per pixel (red, green, blue, 8 bits each).

Source code: `src/devices/dev_fb.c`
Default physical address: 0x12000000

Offset: | Effect:
`0x00000-` `0xe0fff` | Read: read pixel values. Write: write pixel values.

`disk`:

Disk controller, which can read from and write to disk images. It does not use interrupts; read and write operations finish instantaneously.

Source code: `src/devices/dev_disk.c`
Default physical address: 0x13000000

Offset: | Effect:
`0x0000` | Write: Set the offset (in bytes) from the beginning of the disk image. This offset will be used for the next read/write operation.
`0x0010` | Write: Select the SCSI ID to be used in the next read/write operation.
`0x0020` | Write: Start a read or write operation. (Writing `0` means a Read operation, a `1` means a Write operation.)
`0x0030` | Read: Get status of the last operation. (Status 0 means failure, non-zero means success.)
`0x4000-` `0x41ff` | Read/Write: 512 bytes data buffer.

`ether`:

A simple ethernet controller, enough to send and receive packets on a simulated network.

Source code: `src/devices/dev_ether.c`
Default physical address: 0x14000000

Offset: | Effect:
`0x0000-` `0x3fff` | Read/write buffer for the packet to be sent/received.
`0x4000` | Read: status word, one or more of these: `0x01` = something was received (because of the last command) `0x02` = more packets are available NOTE: Whenever the status word is non-zero, an interrupt is asserted. Reading the status word clears it, and deasserts the interrupt.
`0x4010` | Read: get the Length of the received packet Write: set the Length of the next packet to transmit
`0x4020` | Write: command: `0x00:` receive a packet `0x01:` send a packet

While these devices may resemble real-world hardware, they are intentionally made simpler to use. (An exception is the framebuffer; some machines actually have simple linear framebuffers like this.)

If the physical address is `0x10000000`, then for MIPS that means that it can be accessed at virtual address `0xffffffffb0000000`. (Actually it can be accessed at `0xffffffff90000000` too, but devices should usually be accessed in a non-cached manner.)

(When using the PPC test machine (`testppc`), the addresses are `0x10000000`, `0x11000000` etc., so no need to add any virtual displacement.)

The `mp`, `disk`, and `ether` devices are agnostic when it comes to word-length. For example, when reading offset `0x0000` of the `mp` device, you may use any kind of read (an 8-bit read will work just as well as a 64-bit read, although the value will be truncated to 8 bits in the first case). You can not, however, read one byte from `0x0000` and one from `0x0001`, and combine the result. The read from `0x0001` will be invalid.

The `cons` device should be accessed using 8-bit reads and writes. Doing a getchar() (ie reading from offset `0x00`) returns `0` if no character was available.

On MIPS, the `cons` device is hardwired to interrupt 2 (the lowest hardware interrupt). Whenever a character is available, the interrupt is asserted. When there are no more available characters, the interrupt is deasserted. (Remember that the interrupt has to be enabled in the status register of the system coprocessor.)

The `ether` device is hardwired to interrupt 3.

The IPIs controlled by the `mp` device are hardwired to interrupt 6. Whenever an IPI is "sent", interrupt 6 is asserted on the target CPU(s), and the IPI number is added last in the IPI queue for that CPU. It is then up to that CPU to read from offset `0x00c0`, to figure out what kind of IPI it was.

A simple tutorial on how to use the `disk` device, if not clear from the description above, can be found here: `test_disk.c`