ARM Integrator-CP IRQTimerPICTasksMMAndMods

Revision as of 03:54, 8 April 2014 by Pancakes (talk | contribs) (fixed two things)

ARM Integrator-CP IRQ, Timer, PIC, Tasks, Modules, And ELF32

Page Description
IRQ, Timer, And PIC This demonstration just uses the IRQ, Timer, And PIC.
IRQ, Timer, PIC, And Tasks This shows how to switch between tasks using the timer, and builds from the previous page.
IRQ, Timer, PIC, Tasks, And MM This shows how to integrate virtual memory, and builds from the previous page.
Virtual Memory With Paged Kernel This shows a method to have kernel space paged.

Author

I Pancakes wrote this to help jump start you into developing for the ARM using QEMU or even a real piece of hardware. I have wrote software for both emulators and real hardware, but this has only been tested on QEMU so far. Please make any needed changes if you find problems. Also let me know at kmcg3413@gmail.com if you find this useful, have comments, or suggestions.


Intro

You should be coming from one of the previous pages. I prefer the last one if you have been following along. Well, the moment has arrived. You are now ready to see a method of attaching some modules and even a demonstration of loading a ELF32 module into user space.

First we will cover the method to attach files to the end of the kernel image. Then we will look at loading an ELF32 module into user space.

Attaching A Module

Python Script

This scripts pads the module to a 32-bit boundary so reads will be aligned.

#!/usr/bin/python3

import sys
import struct

srcf = sys.argv[1]
dstf = sys.argv[2]
typf = sys.argv[3]

sfd = open(srcf, "rb")
sfd.seek(0, 2)
sz = sfd.tell()
sfd.seek(0, 0)
sd = sfd.read(sz)
sfd.close()

pad = len(sd) & 3
if pad > 0:
	pad = 4 - pad
else:
	pad = 0

print('[ATTACHMOD] %s [%s bytes] ----> %s' % (srcf, sz, dstf))

dfd = open(dstf, "r+b")
dfd.seek(0, 2)
dfd.write(struct.pack('IIII', sz + pad, 0x12345678, 0xedcba987, int(typf)))
dfd.write(sd)
if pad > 0:
	dfd.write(b'A' * pad)
dfd.close()

To attach a module you could do something like this:

    ./attachmod.py mymodule mykernelimage 1

Where 1 is a 32-bit integer value. I use the value one in the kernel code further down to determine if it is an ELF32 module and if so I load it into a newly created user address space.

Module Enumeration Code

#ifndef KMOD_H
#define KMOD_H
#include "stdtypes.h"

typedef struct _KATTMOD {
	uint32			size;
	uint32			signatureA;
	uint32			signatureB;
	uint32			type;
	uint32			slot[];
} KATTMOD;

KATTMOD *kPkgGetNextMod(KATTMOD *mod);
KATTMOD *kPkgGetFirstMod();
uintptr kPkgGetTotalLength();
#endif

#include "kmod.h"

/* symbol created by linker script in kernel image */
extern uint8 _EOI;

/*
	Get the next module after the specified module.
*/
KATTMOD *kPkgGetNextMod(KATTMOD *mod) {
	KATTMOD		*n;

	n = (KATTMOD*)((uintptr)mod + mod->size + sizeof(KATTMOD));
	
	/* make sure there actually exists another module after this one */
	if (n->signatureA != 0x12345678 || ~n->signatureA != n->signatureB) {
		return 0;
	}
	
	return n;
}

uintptr kPkgGetTotalLength() {
	KATTMOD			*m, *lm;
	
	lm = 0;
	for (m = kPkgGetFirstMod(); m; m = kPkgGetNextMod(m)) {
		lm = m;
	}
	
	if (!lm) {
		return (uintptr)&_EOI;
	}
	
	return lm->size + (uintptr)lm;
}

/*
	Locate first module, if any exists, at end of our image.
*/
KATTMOD *kPkgGetFirstMod() {
	uint32		*p;
	uint32		x;
	
	/* get end of our image */
	p = (uint32*)&kPkgGetNextMod;
	
	/* find signature of first module */
	for (x = 0; (uintptr)&p[x] < (uintptr)&_EOI + 0x1000; ++x) {
		/*
			This is a very tricky situation. We do not want the compiler
			to emit these sequence of bytes neither through an immediate
			operand or through optimization.
		*/
		if (p[x] == 0x12345678 && p[x + 1] == ~p[x]) {
			/* backup 32-bits (4 bytes) so we grab the size placed by attachmod.py */
			return (KATTMOD*)&p[x - 1];
		}
	}
	
	return 0;
}

To enumerate modules attached to the image:

        KATTMOD   *m;

	for (m = kPkgGetFirstMod(); m; m = kPkgGetNextMod(m)) {
        }

Loading An Elf Module

The code below is the absolute minimal needed to load an ELF32 into memory. It skips a lot of checks and other advanced features.

ELF Binary Code

int _start(unsigned int *smmio) {
	int		x;

	for (;;) {
		for (x = 0; x < 0xfffff; ++x);
		smmio[0] = 'G';
		smmio[0] = 'K';
	}
	return 0;
}

To compile:

arm-eabi-gcc -nostdlib -nostartfiles -ffreestanding -std=gnu99 main.c -o main -Ttext 0x80000000

Kernel Code

The first part of the kernel code here assumes you have been following along with the other pages in this series of pages. So I am not going to go too deep to explain much of it. But, essentially I check that the module is of type KMODTYPE_ELFUSER and if so then I find a free task slot and hand that to a function that will do the actual loading of the image.

	#define KMODTYPE_ELFUSER			1
	/*
		create a task for any attached modules of the correct type
	*/
	kprintf("looking at attached modules\n");
	for (m = kPkgGetFirstMod(); m; m = kPkgGetNextMod(m)) {
		kprintf("looking at module\n");
		if (m->type == KMODTYPE_ELFUSER) {
			/* find free task structure */
			for (x = 0; x < 0x10; ++x) {
				if (!ks->threads[x].valid) {
					break;
				}
			}
			
			if (x >= 0x10) {
				PANIC("out-of-task-slots");
			}
			
			kelfload(&ks->threads[x], (uintptr)&m->slot[0], m->size);
		}
	}

And, of course the kelfload implementation. This is just a very minimal implementation intended to just get the image into memory as a demonstration and nothing advanced.

typedef uint16 Elf32_Half;
typedef uint32 Elf32_Word;
typedef uint32 Elf32_Off;
typedef uint32 Elf32_Addr;

#define EI_NIDENT 16
typedef struct {
        unsigned char   e_ident[EI_NIDENT];
        Elf32_Half      e_type;
        Elf32_Half      e_machine;
        Elf32_Word      e_version;
        Elf32_Addr      e_entry;
        Elf32_Off       e_phoff;
        Elf32_Off       e_shoff;
        Elf32_Word      e_flags;
        Elf32_Half      e_ehsize;
        Elf32_Half      e_phentsize;
        Elf32_Half      e_phnum;
        Elf32_Half      e_shentsize;
        Elf32_Half      e_shnum;
        Elf32_Half      e_shtrndx;
} ELF32_EHDR;

#define EM_ARM			40

typedef struct {
       Elf32_Word      sh_name;
       Elf32_Word      sh_type;
       Elf32_Word      sh_flags;
       Elf32_Addr      sh_addr;
       Elf32_Off       sh_offset;
       Elf32_Word      sh_size;
       Elf32_Word      sh_link;
       Elf32_Word      sh_info;
       Elf32_Word      sh_addralign;
       Elf32_Word      sh_entsize;
} ELF32_SHDR;

int kelfload(KTHREAD *th, uintptr addr, uintptr sz) {
	ELF32_EHDR			*ehdr;
	ELF32_SHDR			*shdr;
	uint32				x, y;
	uintptr				page, oldpage;
	KSTATE				*ks;
	uint8				*fb;
	
	kprintf("loading elf into memory space\n");
	
	ks = (KSTATE*)KSTATEADDR;
	
	ehdr = (ELF32_EHDR*)addr;
	if (ehdr->e_machine != EM_ARM) {
		kprintf("kelfload: not ARM machine!\n");
		return 0;
	}
	
	if (ehdr->e_ident[4] != 0x1) {
		kprintf("kelfload: not ELF32 object\n");
		return 0;
	}
	
	kvmm2_init(&th->vmm);
	th->pc = ehdr->e_entry;
	th->valid = 1;
	th->cpsr = 0x60000000 | ARM4_MODE_USER;
	/* set stack */
	th->sp = 0x90001000;
	/* pass address of serial output as first argument */
	th->r0 = 0xa0000000;
	/* map serial output mmio */
	kvmm2_mapsingle(&th->vmm, 0xa0000000, 0x16000000, TLB_C_AP_FULLACCESS);
	/* map stack page (4K) */
	kvmm2_allocregionat(&th->vmm, 1, 0x90000000, TLB_C_AP_FULLACCESS);

	/* map address space so we can work directly with it */
	kvmm2_getphy(&ks->vmm, (uintptr)th->vmm.table, &page);
	oldpage = arm4_tlbget1();
	arm4_tlbset1(page);
	
	// e_shoff - section table offset
	// e_shentsize - size of each section entry
	// e_shnum - count of entries in table
	for (x = 0; x < ehdr->e_shnum; ++x) {
		shdr = (ELF32_SHDR*)(addr + ehdr->e_shoff + x * ehdr->e_shentsize);
		if (shdr->sh_addr != 0) {
			/* load this into memory */
			// sh_offset - byte offset in module
			// sh_size - size of section in module 
			// sh_addr - address to load at
			kvmm2_allocregionat(&th->vmm, kvmm2_rndup(shdr->sh_size), shdr->sh_addr, TLB_C_AP_FULLACCESS);
			fb = (uint8*)(addr + shdr->sh_offset);
			/* copy */
			for (y = 0; y < shdr->sh_size; ++y) {
				((uint8*)shdr->sh_addr)[y] = fb[y];
			}
		}
	}
	
	/* restore previous address space */
	arm4_tlbset1(oldpage);
}

Of course it is just minimal but it should be enough for you to get started with and improve it!

Full Source

You can access the full source with:

    git clone http://kmcg3413.net/armthin.git
    git checkout origin/bINT_TIMER_PIC_TASKS_VMM_MOD_ELF

The head (master branch) may contain more work beyond this page.

Going Further

At this point you have almost a fully functional kernel. Your missing better support for device drivers, services, and such things. Your also at the point where you can build different kernel types depending on your design. Your missing system calls for user space for memory allocation. Also, you need processes and threads instead of the more primitive task like design we have now. Also, you need to handle exceptions for user space correctly such as terminating the offending process or thread. Also, synchronization primitives and common services like timers and sleeping are missing. And, then again maybe you do not need these and you have some really awesome design!? Well, code to your heart's content is my suggestion!

You can continue along with me or branch out in your own direction if you like, but in the next page we are going to create actual processes and threads, improve our exception handling, and implement IPC. I am going to go the micro-kernel route as I really like the design even if it might have some disadvantages.

For lack of a better name, continue onward to, Phase 2.