Linux and the BIOS

The x86 kernel has direct and indirect dependencies on the system BIOS. Here’s a look at some of the guts.
Parts of the x86 kernel, such as Advanced Power Management (APM) and the video framebuffer driver, vesafb, explicitly use BIOS services to realize certain features. Other sections of the kernel like the serial driver, implicitly depend on the BIOS to initialize I/O base addresses and interrupt levels, and real-mode kernel code makes extensive use of BIOS calls during boot up to perform tasks like assembling the system memory map. (On BIOS-less embedded architectures, similar responsibilities — for example, waking the kernel from suspend on ARM Linux — rest with the bootloader.)
In this month’s “Gearheads” column, let’s learn to interact with the BIOS using some examples. But first, let’s look at the different facets of assembly programming on Linux.

A Peek into Linux Assembly

Figure One shows the Linux boot sequence on an x86-based desktop or laptop. The firmware components in the figure are usually implemented using different assembly syntaxes:
*The BIOS is typically wholly written in assembly. Some of the popular BIOSes are coded using assemblers like the Microsoft Macro Assembler (MASM).
*Linux bootloaders like LILO and GRUB are implemented using a mix of C and assembly. The SYSLINUX bootloader is written entirely in assembly using the Netwide Assembler (NASM).
*Real-mode Linux startup code uses the GNU Assembler (GAS).
*Protected-mode BIOS invocations are done in inline assembly, which is a construct supported by the GNU Compiler Collection (GCC) to insert assembly in between C statements.
The former two generally follow Intel-based assembly syntax, while the latter two are coded in AT& T (or GAS, http://sig9.com/articles/att-syntax/) syntax. There are some exceptions, however: the assembly parts of GRUB use GAS.
To illustrate the difference between these syntaxes, consider a code snippet that outputs a byte to the parallel port. In Intel format, used by the BIOS or the bootloader, you’d write:
mov dx, 03BCh   /* 0x3BC is the I/O address of the parallel port */
mov al, 0ABh /* 0xAB is the data to be output */
out dx, al /* Send */
However, if you want to do the same from Linux real-mode startup code (say, arch/i386/boot/setup.S), you’d write:
movw $0x3BC, %dx
movb $0xAB, %al
outb %al, %dx
Unlike Intel format, the source operand in AT& T syntax comes first and the destination operand comes second (which seems to be the natural direction since we read from left to right). Register names in AT& T format are preceded by %(” percent”) and immediate operands are preceded by $(” dollar sign”). AT& T opcodes have suffixes like b and w to specify the size of memory operands, while Intel syntax accomplishes this by looking at the operands instead of the opcodes. Had you been moving pointer references instead of immediate values in Intel syntax, you’d have had to specify operand prefixes such as byte ptr.
The advantage of learning AT& T syntax is that it’s understood by GAS and inline GCC, which work not just on Intel-based systems, but on a variety of processor architectures.
Next, let’s write the above snippet using GCC inline assembly, which is what you’d use from the protected mode kernel:
unsigned short port = 0x3BC;
unsigned char data = 0xAB;

"outb %%al, %%dx\n\t"
: "a" (data), "d" (port)
The general format of the asm construct supported by GCC is as follows:
asm ( assembly
: output operand constraints
: input operand constraints
: clobbered operand specifier
In the operand sections, a, b, c, d, S and D stand for EAX, EBX, ECX, EDX, ESI, and EDI registers, respectively. Input operand constraints copy data from the supplied variables to the specified registers before executing the assembly instructions, while output operand constraints (written as =a, =b, and so on) copy data from the specified registers to the supplied variables after executing the assembly instructions. The clobbered operand constraints ask gcc to assume that the listed registers aren’t available for use.
The only constraint used in the example above is specific to input operands. This effectively copies the value of data to the AL register and the value of port to the DX register. Register names are preceded by %% in inline assembly, since % is used to refer to the supplied operands. Thus, if you want to refer to data and port inside the example inline assembly, you can use %0 and %1, respectively.
To get a clearer picture of inline assembly translation, look at Listing One, the corresponding assembly code generated by gcc –s.
LISTING ONE: Assembly code translation created by gcc –s

movw $956, -2(%ebp) /* Value of data in stack is set to 0x3BC */
movb $-85, -3(%ebp) /* Value of port in stack is set to 0xAB */
movb -3(%ebp), %al /* movb 0xAB, %al */
movw -2(%ebp), %dx /* movw 0x3BC, %dx */
#APP /* Marker to note start of inline assembly */
outb %al, %dx /* Write to parallel port */
#NO_APP /* Marker to note end of inline assembly */

Making Real Mode BIOS Calls

Many sections of the kernel glean information from the BIOS in real-mode and use the collected information during normal operation in protected-mode.
The steps needed to accomplish this are:
1.Real-mode kernel code invokes BIOS services and populates the returned information in the first physical memory page, called the zero page. This is done in arch/i386/boot/setup.S. The full layout of the zero page can be found in Documentation/i386/zero-page.txt.
2.After the kernel switches to protected mode, but before it clears the zero page, the obtained data is saved in kernel data structures. This is done in arch/i386/kernel/setup.c.
3.The protected-mode kernel makes appropriate use of the saved information during normal operation.
As an example, let’s see how the kernel assembles the system memory map from the BIOS. Listing Two is a snippet from arch/i386/boot/setup.S that invokes the BIOS int 0×15 service to obtain the system memory map.
Listing Two: Obtaining the system memory map

movw $E820MAP, %di # Desired offset in zero page

movl $0x0000e820, %eax # BIOS function number
movl $SMAP, %edx # Ascii ’SMAP’
movl $20, %ecx # Size of a map element
pushw %ds
popw %es
int $0×15 # Invoke BIOS service

# ..Error Checks.. #

movb (E820NR), %al
cmpb $E820MAX, %al # Check for max entries
jnl bail820

incb (E820NR) # Bump up entry counter
movw %di, %ax
addw $20, %ax
movw %ax, %di
cmpl $0, %ebx # Are we at the last entry?
jne jmpe820

In the listing, 0xE820 is the function number that needs to be specified in the AX register before invoking int 0×15 to procure the memory map. If you look at the BIOS call definition for int 0×15, function 0xE820 (the full list is available at http://lrs.fmi.uni-passau.de/support/doc/interrupt-57/INT.HTM), you’ll see that the BIOS writes the current element of the memory map in a buffer pointed to by the ES:DI register. So, in Listing Two, the ES:DI register is made to point to the offset in the zero page where the memory map is to be stored. This offset is called E820MAP and is defined in include/asm-i386/e820.h.
The code then loops until all of the elements in the memory map are collected. The number of elements is computed and stored at offset E820NR in the zero page. When execution successfully exits the loop, the memory map is available in the zero page in the form of struct e820map, defined in include/asm-i386/e820.h:
struct e820map {
int nr_map;
struct e820entry {
/* start of memory segment */
unsigned long long addr;
/* size of memory segment */
unsigned long long size;
/* type of memory segment */
unsigned long type;
} map[E820MAX];
The kernel switches to protected-mode later on in arch/i386/boot/setup.S. Once in protected mode, the kernel saves the collected memory map via the function copy_e820_map(), defined in arch/i386/kernel/setup.c and shown in Listing Three (for brevity, error checks have been elided and the add_memory_region() routine has been folded):
Listing Three: Copying the memory map

/* Input parameters biosmap and nr_map point
* respectively to offsets E820MAP and E820NR in the zero page
* described earlier
static int __init
copy_e820_map (struct e820entry * biosmap, int nr_map)
/* … */

do {
/* Copy memory map information collected
* from the BIOS into local variables
unsigned long long start = biosmap->gt;addr;
unsigned long long size = biosmap->gt;size;
unsigned long long end = start + size;
unsigned long type = biosmap->gt;type;

/* .. Sanitize start and size .. */

/* Populate the kernel data structure, e820 */
e820.map[x].addr = start;
e820.map[x].size = size;
e820.map[x].type = type;
} while (biosmap++,–nr_map); /* Do for all elements in the map */

/* … */

Look at arch/i386/mm/init.c to see how the e820 data structure populated above is used later on in the boot process.
To take another example, the kernel makes use of the BIOS int 0×10 service to get video mode parameters in arch/i386/boot/video.S. The VESA framebuffer driver, drivers/video/vesafb.c. relies on this BIOS call to turn on graphics mode at boot time. Bootloaders also make use of BIOS services in real mode. If you look through the sources of LILO, GRUB or SYSLINUX, you can see a liberal sprinkling of int 0×13 calls to read the kernel image from the boot device.
(As an exercise, use a similar approach to grab POST error codes from the BIOS from the real-mode kernel, via int 0×15, function 0×2100, and display them during normal operation via the /proc filesystem.)

Making Protected-Mode BIOS Calls

To see how the kernel makes protected mode BIOS calls, let’s look at the APM implementation.
APM is a BIOS interface specification. Power management policies are defined in the BIOS, and a kernel thread (called kapmd) polls it every second to figure out the course of action. The polling is done using protected mode BIOS calls. To do this, kapmd needs to know the protected mode entry segment address and offset. These are obtained from the real mode kernel (arch/i386/boot/setup.S) during boot up using the int 0×15, function 0×5303) BIOS service.
The actual protected mode BIOS call is invoked using inline assembly found in the function apm_bios_call_simple_asm() and defined in include/asm-i386/mach-default/apm.h. The code is reproduced in Figure Four.
LISTING FOUR: Invoking a BIOS call with inline assembly

__asm__ __volatile__(APM_DO_ZERO_SEGS
“pushl %%edi\n\t”
“pushl %%ebp\n\t”
“lcall *%%cs:apm_bios_entry\n\t”
“setc %%bl\n\t”
“popl %%ebp\n\t”
“popl %%edi\n\t”
: “=a” (*eax), “=b” (error), “=c” (cx), “=d” (dx),
“=S” (si)
: “a” (func), “b” (ebx_in), “c” (ecx_in)
: “memory”, “cc”);

apm_bios_entry contains the protected mode entry address. The input constraint “a”(func), copies the desired BIOS function number to the EAX register before invocation.
For example, function number APM_FUNC_GET_EVENT (0x530b) elicits an APM event from the BIOS, and function APM_FUNC_IDLE (0×5305) notifies the BIOS that the processor is idle.
Invocation results are returned by the BIOS in registers EAX, EBX, ECX and EDX. As per the output operand constraints above, these are propagated to the caller in variables *eax, error, cx, and dx, respectively. In the assembly body, registers are saved onto the kernel stack before the BIOS call and are restored afterwards to prevent the BIOS from trampling over them.
(For a fuller understanding of how APM is implemented in x86 Linux, look at arch/i386/kernel/apm.c, include/linux/apm_bios.h, and include/asm-i386/mach-default/apm.h in the kernel tree. If you’re curious to know how APM is implemented on embedded BIOS-less architectures, take a look at arch/arm/kernel/apm.c.)

BIOS and Legacy Linux Drivers

The BIOS provides a degree of hardware abstraction to some Linux drivers. Consider the serial driver as an example.
The BIOS probes the SuperIO chipset and assigns I/O base addresses and IRQs for the respective serial (and infrared) ports. The serial driver needs to be told about the resources assigned by the BIOS, either via hard-coded values in a header file (include/asm-i386/serial.h) or via user-space commands. (As an exercise, you can dig into the data sheet of your SuperIO chipset and add support in the serial driver to probe for the resource values set by the BIOS.)
To take another example, even if you disable Universal Serial Bus (USB) support in the kernel, you can use USB keyboards and mice, provided that your BIOS has built-in USB support. The BIOS turns on an emulation mode that routes USB keyboard and mouse input from the USB controller to the keyboard controller. This tricks the operating system into thinking that you are using a legacy keyboard or mouse.
If you are tweaking the BIOS and the kernel to suit your x86-based device, a kernel driver might need to sweep the BIOS space to locate certain Vital Product Data (VPD) information. This can be done with code like that shown in Listing Five.
Listing Five: Sweeping the BIOS area for some information

/* Format of your information present in the BIOS area */
struct my_device_vpd {
unsigned short magic; /* Identification signature */
/* … VPD fields follow … */
} * baddr;
unsigned short * bios_data;

/* … */

* Search the permissible BIOS address range (0xE0000-0xFFFF0)
for (baddr = (unsigned short *) (__va(0xE0000));
baddr <= (unsigned short *) (__va(0xFFFF0));
baddr++) {
bios_data = (struct my_device_vpd *)baddr;

/* Assume that 0xABCD is the signature */
if (bios_data->magic == 0xABCD) {
* The data fields follow the magic.
* Copy them to a kernel structure..
} else {


To debug the real-mode kernel, you can’t use debuggers like the Kernel Debugger (kdb) or the Kernel GNU Debugger (kgdb).
A quick way to debug kernel assembly snippets is by using the DOS debug tool after converting your code into Intel-style syntax. But debug was created in the 16-bit era, so you can’t, say, step through code that initializes the EAX register. Hardware assisted JTAG-based debuggers are a kind of panacea, since it can be used to debug the BIOS, the bootloader, Linux real-mode code, and kernel-BIOS interactions.

Sreekrishnan Venkateswaran has been working for IBM India since 1996. His recent Linux projects include putting Linux onto a wristwatch, an MP3 player, and a pacemaker programmer. You can reach Krishnan at class="emailaddress">krishhna@gmail.com.

Comments are closed.