Intermediate
Linux 6.x
2 of 3
The Linux kernel runs on dozens of processor architectures — 64-bit x86, 32-bit ARM, 64-bit ARM (AArch64), RISC-V, MIPS, PowerPC, and more. Each of these platforms has different word sizes and different rules about how large types like size_t are represented.
If you use the wrong format specifier in a printk() call, you either get garbled output or compiler warnings. This tutorial teaches you the correct format specifiers to use for every common type in kernel code, so your module logs correctly on every architecture without modification.
1. The Portability Problem in Kernel Logging
In standard C user-space code you often use %lu for an unsigned long or %d for an int. This works when your code only runs on one architecture. The moment your kernel module needs to work on both a 32-bit ARM board (where unsigned long is 4 bytes) and a 64-bit x86 server (where it is 8 bytes), using %lu for a size_t variable will produce wrong output or compiler warnings on one of the two platforms.
%d or %u for size_t variables. On a 64-bit kernel, size_t is 8 bytes but %u only reads 4 bytes, causing the wrong value to be printed and potentially corrupting subsequent arguments in the same printk() call.
2. size_t and ssize_t — The Most Common Pitfall
These two types are widely used across kernel APIs. size_t represents the size of a memory region or a buffer and is always unsigned. ssize_t is the signed counterpart, used for return values from functions that can return either a byte count or a negative error code.
Their actual underlying type varies by architecture:
| Architecture | size_t width | Equivalent C type |
|---|---|---|
| 32-bit ARM (armv7) | 4 bytes | unsigned int |
| 64-bit ARM (AArch64) | 8 bytes | unsigned long |
| x86 32-bit | 4 bytes | unsigned int |
| x86-64 | 8 bytes | unsigned long |
| RISC-V 64-bit | 8 bytes | unsigned long |
The correct format specifiers to use regardless of architecture:
size_t bytes_written = get_buffer_usage();
ssize_t result = read_from_device();
/* CORRECT — portable on all architectures */
pr_info("bytes written: %zu\n", bytes_written);
pr_info("read result: %zd\n", result);
/* WRONG — may fail on 64-bit systems */
pr_info("bytes written: %u\n", bytes_written); /* truncates on 64-bit */
pr_info("read result: %d\n", result); /* truncates on 64-bit */
%zu for size_t and %zd for ssize_t. The z length modifier tells the compiler to match the size of the argument to the platform’s size_t width automatically.
3. Kernel Pointer Format Specifiers
Printing raw memory addresses in kernel logs is more complex than in user-space code because the kernel must balance two competing concerns: debugging usefulness and security. Exposing real kernel virtual addresses in the log leaks information about kernel memory layout, which helps attackers defeat KASLR (Kernel Address Space Layout Randomisation).
Linux 6.x provides three distinct pointer format specifiers for different situations:
| Specifier | What it prints | When to use | Production safe? |
|---|---|---|---|
| %pK | Hashed or zeroed address depending on privilege | Proc files, sysfs, any user-visible path | Yes |
| %px | Actual raw address in hex | Local debug sessions only, never shipped | No |
| %p | Hashed value (since Linux 4.15+) | Avoid — output is not meaningful for debugging | Partially |
void *kernel_buf = kmalloc(256, GFP_KERNEL);
phys_addr_t phys = virt_to_phys(kernel_buf);
/* Safe in production — hashed address */
pr_info("buffer at: %pK\n", kernel_buf);
/* For physical addresses — must pass by reference */
pr_info("physical address: %pa\n", &phys);
/* DEBUG ONLY — prints real address, remove before shipping */
pr_debug("raw debug address: %px\n", kernel_buf);
%p specifier hashes the pointer value. The hash changes every boot, so two different pointers may print the same hashed value and one pointer will print a different hashed value on each reboot. It is not useful for debugging. Always use %pK instead, or %px only in local debug builds.
4. Physical Addresses With %pa
In device driver work you frequently deal with physical memory addresses — the addresses your hardware DMA engine uses to access memory directly. These are represented as phys_addr_t in the kernel, a type whose width depends on the physical address space of the SoC.
The %pa specifier handles this correctly across all platforms, but it requires you to pass the address by pointer (a pointer to the phys_addr_t variable), not by value. This is different from all other format specifiers and is easy to get wrong.
/* Printing a physical address correctly */
phys_addr_t dev_base = 0x40020000; /* example peripheral base address */
/* CORRECT — pass by reference */
pr_info("peripheral base: %pa\n", &dev_base);
/* WRONG — passing by value causes wrong output */
pr_info("peripheral base: %pa\n", dev_base); /* BUG: missing & */
5. Printing Raw Buffers as Hex Data
When debugging protocol drivers or memory content, you often want to dump a block of bytes as a hex string in the kernel log. The %*ph specifier does this for short buffers.
unsigned char packet[8] = {0x01, 0x02, 0xAB, 0xCD, 0x00, 0xFF, 0x10, 0x20};
/* Print up to 64 bytes as hex — width given by first argument */
pr_info("packet: %*ph\n", (int)sizeof(packet), packet);
/* Output: packet: 01 02 ab cd 00 ff 10 20 */
/* Compact format — no spaces between bytes */
pr_info("compact: %*phC\n", (int)sizeof(packet), packet);
%*ph specifier is intended for buffers up to 64 bytes. For larger memory regions, use the print_hex_dump_bytes() kernel function, which handles pagination and produces neatly formatted output with both hex and ASCII columns, similar to what xxd or hexdump -C produces in user space.
#include <linux/printk.h>
/* Dumping a large buffer — for more than 64 bytes */
void dump_my_buffer(const void *buf, size_t len)
{
print_hex_dump_bytes("mydriver: ", DUMP_PREFIX_OFFSET, buf, len);
/* Produces multi-line output with offset, hex, and ASCII columns */
}
6. Network Address Format Specifiers
Network drivers and protocol stacks frequently need to log IP addresses. Using a raw integer with %u or %x gives an unreadable number. Linux provides dedicated specifiers that produce human-readable dotted-decimal (IPv4) and colon-separated (IPv6) notation directly.
#include <linux/inet.h>
/* IPv4 — pass a pointer to the address, NOT the value */
__be32 ipv4_addr = in_aton("192.168.1.100");
pr_info("source IP: %pI4\n", &ipv4_addr);
/* Output: source IP: 192.168.1.100 */
/* IPv4 in little-endian byte order */
pr_info("source IP LE: %pI4b\n", &ipv4_addr);
/* IPv6 — pass a pointer to struct in6_addr */
struct in6_addr ipv6_addr;
pr_info("IPv6 addr: %pI6\n", &ipv6_addr);
/* Output: IPv6 addr: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 */
/* IPv6 compressed form — collapses consecutive zero groups */
pr_info("IPv6 compressed: %pI6c\n", &ipv6_addr);
| Specifier | Address type | Example output |
|---|---|---|
| %pI4 | IPv4 (big-endian) | 192.168.1.100 |
| %pI4b | IPv4 (little-endian) | 100.1.168.192 |
| %pI6 | IPv6 full | 2001:0db8:85a3:0000:… |
| %pI6c | IPv6 compressed | 2001:db8:85a3::8a2e:370:7334 |
| %pM | MAC address (colon-separated) | 01:23:45:67:89:ab |
| %pMR | MAC address (reversed byte order) | ab:89:67:45:23:01 |
7. Complete Portable Format Specifier Reference
| C type | Correct specifier | Why |
|---|---|---|
| int | %d | Standard signed integer — always 4 bytes in kernel |
| unsigned int | %u | Standard unsigned integer |
| long | %ld | Signed long — width varies by arch |
| unsigned long | %lu | Unsigned long |
| size_t | %zu | Portable — matches platform size_t width |
| ssize_t | %zd | Portable — signed size |
| u64 / __u64 | %llu | 64-bit unsigned — always use %llu not %lu |
| s64 / __s64 | %lld | 64-bit signed |
| void * (safe) | %pK | Hashed — safe for production |
| void * (debug) | %px | Actual address — debug only, not for production |
| phys_addr_t | %pa | Physical address — must pass by reference (&addr) |
| __be32 (IPv4) | %pI4 | IPv4 dotted-decimal notation |
| struct in6_addr * | %pI6c | IPv6 compressed notation |
| u8[] (short buf) | %*ph | Hex dump, buffers up to 64 bytes |
8. Why %p Alone Is Dangerous Since Linux 4.15
Before Linux 4.15, using %p in a printk() call printed the raw kernel virtual address. This was a significant security hole because KASLR (Kernel Address Space Layout Randomisation) randomises where the kernel loads each boot, and exposing real addresses in the kernel log allows an attacker who can read dmesg to trivially bypass KASLR.
Starting with Linux 4.15, plain %p prints a one-way hash of the pointer instead of its real value. The hash is consistent within one boot session but changes every reboot. This means:
- Two different pointers may hash to the same printed value
- The same pointer prints a different value after each reboot
- You cannot use the output to compute the real address
- The output is useless for debugging
%p in kernel code. Use %pK for any pointer you need to expose safely, or %px strictly in local debug code that will never be merged or shipped to a production system.
Interview Questions & Answers
size_t is 8 bytes wide. The %u specifier only reads 4 bytes from the argument. This causes the wrong value to be printed and, because the additional 4 bytes are left in the argument list, all subsequent arguments in the same printk() call may also be misread. The portable fix is to use %zu, which the compiler maps to the correct width for size_t on the target architecture.%pK specifier mitigates this by printing a hashed or zeroed representation of the pointer instead of its actual value, so no real address information leaks into the kernel log.%pa specifier is designed to work portably across architectures where phys_addr_t may be wider than the platform’s native register size. To ensure the full value is correctly read regardless of how the C ABI passes arguments of different widths, the kernel convention requires the address to be passed as a pointer to the variable. This guarantees the specifier reads the correct number of bytes on both 32-bit and 64-bit systems.%pI4 formats an IPv4 address stored as a 32-bit big-endian integer (__be32) into dotted-decimal notation such as 192.168.1.1. %pI6c formats an IPv6 address stored as a struct in6_addr into compressed colon-separated notation, collapsing consecutive groups of zero fields into :: according to RFC 5952 rules. Both require the address to be passed by pointer.%*ph is a format specifier that dumps up to 64 bytes as a single-line hex string within a pr_info() or similar call. For buffers larger than 64 bytes, it is truncated. print_hex_dump_bytes() is a dedicated kernel function that handles arbitrarily large buffers, splitting them into multiple log lines with a user-defined prefix, byte offset column, hex column, and ASCII printable column — similar to the output of hexdump -C in user space.Free Linux Kernel & Device Driver Course
Next: Learn how the kernel’s Kbuild system compiles your module with the correct Makefile.
Next: Kernel Module Makefile → ← Back: printk & pr_fmt()