Thursday, September 17, 2009

Art of Kernel Driver Hacking

Few points that i have stolen from HOWTO: Linux Device Driver Dos and Don'ts

Efficient error handling, reporting and recovery:


You can follow some simple and proven ways to handle errors and faults within your code:

Use printk calls :

KERN_* are defined in linux/include/linux/kernel.h as follows:

#define KERN_EMERG "<0>" /* system is unusable */
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING"<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant condition */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */

Note: Use printk only when necessary. Be informative and avoid flooding the console
with error messages when error condition is detected (flood control).



Goto statements are advocated in the Linux community for the following reasons:
-Uses less repeated code.
-Is just as readable as other methods.
-Is less error-prone.
-Easier to change/maintain for the next person who has to tweak your code.
-Keeps the error handling code out of the way for more efficiency in cache utilizations, having
(normally) more frequent successful code be a fall through and thus avoiding jumps.
-Used across the kernel code and using goto makes error handling uniform
across the kernel.




Your device driver should always return a status for a request it received.
It is advocated you always use enumerated return codes as well as normal return
codes. Returning 0 = pass 1 or -1 = failed is vague and could be misleading.

All standard errors returned from driver code should use the standard errors
provided by the Linux kernel header files in:
linux/include/linux/errno.h
linux/include/asm/errno.h
linux/include/asm-generic/errno-base.h
linux/include/asm-generic/errno.h


A device driver should do everything within it's capabilities to avoid calling
panic(). The fact that a device (hardware) is broken, non-responsive, and/or
faced an unresolved condition; doesn't mean the device driver should call panic().

Variable declaration and initialization.

-For optimal assembly code output, declaring
[const] char foo[] = "blah";
is better than
[const] char *foo = "blah";
since the later would generate two variables in final
assembly code, a static string and a char pointer to it.

-"Unsigned int" is preferred to "int" because it
generates better asm code on all platforms except sh5.
However, if needed for negative error return codes,
"int" is sufficient.

-If you have a pointer variable to a struct and need to use
"sizeof", it's better to use "sizeof(*var)" than "sizeof(type)".
This is a maintenance issue. If the type is changed, you don't
have to search for all instances of "sizeof(type)" and update
them.

Balancing functions:

It is important to make sure all your function calls for resource
allocation/release are balanced and matched on the other end and
during or after failures.

The obvious memory allocation using kmalloc and kfree as well as
kfree_skbs when freeing skbs.

Common areas found needing attention are:
-pci_alloc_consistent() and pci_free_consistent().
-ioremap() must be balanced by an iounmap(). This is a common memory
leak.
-Functions with *_lock_* naming should be balanced with their *_unlock_*
partner to void dead-locks.

All I/O space access should use the C I/O functions instead of assembly code.

These functions are:
Byte I/O read/writes:
unsigned inb(unsigned port); //read
void outb(unsigned char byte, unsigned port); //write

Word I/O read/writes:
unsigned inw(unsigned port); //read
void outw(unsigned short word, unsigned port); //write

Long I/O read/writes:
unsigned inl(unsigned port); //read
void outl(unsigned doubleword, unsigned port); //write

String versions of the I/O space I/O Access:
void insb(unsigned port,void *addr,unsigned long count); //read
void outsb(unsigned port,void *addr,unsigned long count); //write
void insw(unsigned port,void *addr,unsigned loing count); //read
void outsw(unsigned port,void *addr,unsigned long count); //write
void insl(unsigned port,void *addr,unsigned long count); //read
void outsl(unsigned port,void *addr,unsigned long count); //write

Example:
__asm__( "mov $0, %al\n\t" "outb %al, $0x70");
should be:
outb(0x00, 0x70);

All memory mapped I.O access should use read()/write() functions instead of de-referencing a pointer.


These functions are:
unsinged readb(address);
unsigned readw(address);
unsinged readl(address);

void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);

Example:
*(u8 *) (IOBASE+IER) = 0x80;
should be:
writeb(0x80, IOBASE+IER);


Note: you have to use the -O2 optimization flag while compiling for
these functions (macros really) to expand.