Linux Kernel printk() Format Specifiers: Portable Logging

printk Format Specifiers
Writing portable, architecture-independent kernel log messages — Linux 6.x Edition
Skill Level
Intermediate
Kernel
Linux 6.x
Part
2 of 3
Series: PREV_LEC NEXT_LEC
Topics Covered:
printk format specifiers kernel pointer logging size_t ssize_t kernel %pK %px kernel portable kernel code free linux kernel programming course free linux device driver course printk IPv4 IPv6

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.

Common mistake: Using %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:

size_t and ssize_t width 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 */
Rule: Always use %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:

Kernel pointer format specifiers and when to use them
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);
Security warning: Since Linux 4.15, using the plain %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);
Longer buffers: The %*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);
Network address format specifier quick reference
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

printk format specifiers quick reference — Linux 6.x
C type Correct specifier Why
int%dStandard signed integer — always 4 bytes in kernel
unsigned int%uStandard unsigned integer
long%ldSigned long — width varies by arch
unsigned long%luUnsigned long
size_t%zuPortable — matches platform size_t width
ssize_t%zdPortable — signed size
u64 / __u64%llu64-bit unsigned — always use %llu not %lu
s64 / __s64%lld64-bit signed
void * (safe)%pKHashed — safe for production
void * (debug)%pxActual address — debug only, not for production
phys_addr_t%paPhysical address — must pass by reference (&addr)
__be32 (IPv4)%pI4IPv4 dotted-decimal notation
struct in6_addr *%pI6cIPv6 compressed notation
u8[] (short buf)%*phHex 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
Bottom line for Linux 6.x: Never use bare %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

Q1. Why can’t you use %u for size_t in a printk() call on a 64-bit kernel?
A: On a 64-bit kernel, 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.
Q2. What is KASLR and how does it relate to the %pK specifier?
A: KASLR (Kernel Address Space Layout Randomisation) loads the kernel at a random base address each boot. An attacker who learns real kernel virtual addresses from the log can calculate the base address and then compute the location of any kernel function or data structure, defeating KASLR. The %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.
Q3. When using %pa for a phys_addr_t, why must you pass the address by pointer?
A: The %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.
Q4. What is the difference between %pI4 and %pI6c?
A: %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.
Q5. What is print_hex_dump_bytes() and when should you use it instead of %*ph?
A: %*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()
← Previous EmbeddedPathashala — Free Linux Kernel & Device Driver Course Next →

Leave a Reply

Your email address will not be published. Required fields are marked *