Patching the kernel callout code

Updated: April 19, 2023

If a device for which you are writing a kernel callout can appear in different locations on different boards, a patch routine is required to add the addresses of the registers to the kernel callout code.

There are two reasons why kernel callouts may not know what addresses the registers occupy:

The kernel callout code is part of the startup library, and has therefore been designed to be flexible; it doesn't hard-code the register addresses but instead assumes that the addresses are patched in.
These addresses come from the board-specific code; that is, the addresses are found in code in a board's directory, not in the startup library code, which is designed to be as board-independent as possible.
Address conversion
A physical-to-virtual address conversion is required, which means the kernel callout can’t know the virtual address where the register is mapped in until after the kernel has copied the callouts into system memory.
Note: If the only registers that the callout code accesses are CPU registers, patching isn't needed.

Patcher routines

When it starts up, the procnto kernel and process manager runs any patchers that are present. The third argument of the CALLOUT_START macro can be either 0 (nothing to do), or the address of a patcher routine.

Patcher routines have the following prototype:

void patcher( paddr_t paddr, 
	paddr_t vaddr, 
	unsigned rtn_offset,
	unsigned rw_offset,
	void *data,
	struct callout_rtn *src );

A patcher routine is invoked immediately after the callout has been copied to its final location. Its arguments are:

The physical address of the start of the system page.
The virtual address of the system page that allows read/write access (usable only by the kernel).
The offset from the start of the system page to the start of the kernel callout's code.
The offset from the start of the system page to a location of read/write storage, which can be shared by all kernel callouts that have the same value in their CALLOUT_START macro's second argument (see Allocating read/write storage in this chapter).
A pointer to arbitrary data registered by callout_register_data().
A pointer to the callout_rtn structure that's being copied into place.

Patcher routines don't have to be written in assembly. They are usually written in assembly, however, so they can be kept in the same source file as the code that they patch.

If you arrange the first instructions in a group of related callouts in the same way (e.g. debug_char_*(), poll_key_*(), break_detect_*()), you can use the same patcher routine for these callouts.

Example patcher routine

Here's an example of a patcher routine for an x86 processor. We assume that the display_char_8250() routine has been copied to its permanent location in memory before we invoke our patcher.

The patch_debug_8250() modifies the constants in the first two instructions to the I/O port location and register spacing required for the board:

    movl    0x4(%esp),%eax              // get paddr of routine
    addl    0xc(%esp),%eax              // ...
    movl    0x14(%esp),%edx         	// get base info

    movl    DDI_BASE(%edx),%ecx         // patch code with real serial port
    movl    %ecx,0x1(%eax)
    movl    DDI_SHIFT(%edx),%ecx        // patch code with register shift
    movl    $REG_LS,%edx
    shll    %cl,%edx
    movl    %edx,0x6(%eax)

CALLOUT_START(display_char_8250, 0, patch_debug_8250)
    movl      $0x12345678,%edx          // get serial port base (patched)
    movl      $0x12345678,%ecx          // get serial port shift (patched)