Standalone · Kernel

Anatomy of a CUDA binary

When you compile a CUDA kernel, the final artifact is a cubin: a standard ELF64 file with NVIDIA-specific sections that encode everything the driver needs to load and launch a kernel. The machine code, the parameter layout, register allocation metadata, and a collection of attributes with no public documentation.

When you compile a CUDA kernel, the final artifact is a cubin, a CUDA binary. It is a standard ELF64 file with NVIDIA-specific sections that encode everything the CUDA driver needs to load and launch a kernel: the machine code, the parameter layout, register allocation metadata, and a collection of attributes that have no public documentation.

A note on methodology: everything here is based on analysis of cubins produced by ptxas and validated on real hardware. NVIDIA does not publish a specification for the cubin section layout, the .nv.info EIATTR encoding, or the constant bank parameter conventions. The structures and semantics are reverse-engineered from compiled binaries.

The ELF container

A cubin is an ELF64 executable. The header identifies it:

e_ident[EI_OSABI]    = 0x41       (CUDA ABI, not the older 0x33)
e_ident[EI_ABIVERSION] = 8
e_type               = ET_EXEC    (loadable, not relocatable)
e_machine            = EM_CUDA    (0xBE)
e_flags              = 0x06006402 (for sm_100, 64-bit addressing)

The e_flags field encodes the SM architecture in bits 8-15. For sm_100 (B200), that is 0x64 = 100 decimal. Bit 1 marks 64-bit addressing. Bits 24-26 carry a format version that the driver checks.

Older cubins used ELFOSABI_CUDA = 0x33 with ABI version 0. The CUDA 13 driver rejects these: cuModuleGetFunction returns CUDA_ERROR_NOT_SUPPORTED. If you are emitting cubins from scratch, the ABI version matters.

Section layout

A single-kernel cubin contains roughly twelve sections. Using a minimal kernel, an integer add written in LLVM IR:

define i32 @add(i32 %a, i32 %b) {
  %c = add i32 %a, %b
  ret i32 %c
}

After compiling to a cubin, the sections are:

This is the minimal set. A real cubin produced by ptxas for a production kernel is larger, typically ~22 sections, 8 symbols, and 5 program headers. One notable addition is .nv.shared.reserved.0, a SHT_NOBITS section (64 bytes) that reserves shared memory space. Other additions include .nv.shared. (shared memory allocation), .nv.global (global variable metadata), .nv.constant2. (compiler-generated constant data), .debug_* sections (DWARF debug info), and .rel.* relocation sections.

.text.: the machine code

The .text.add section contains the SASS instructions. Each instruction is a 128-bit (16-byte) word. The section flags are SHF_ALLOC | SHF_EXECINSTR: SHF_ALLOC means the section occupies memory at runtime (the driver must load it onto the GPU), and SHF_EXECINSTR marks it as containing executable machine instructions. The section is aligned to 128 bytes.

Bits   0..104:  instruction body (opcode, operands, modifiers)
Bits 105..121:  scheduling control (stall, yield, barriers)
Bits 122..125:  operand reuse flags

The kernel's entry symbol is a STT_FUNC in the symtab with st_other = 0x10 (STO_CUDA_ENTRY), pointing at the start of this section. The driver uses STO_CUDA_ENTRY to distinguish kernel entry points from device functions.

.nv.constant0.: the constant bank

Kernel parameters are passed through constant bank 0. The section .nv.constant0.add is a SHT_PROGBITS section sized to cover the parameter region. The param_base is set by the CUDA toolkit, not a fixed ISA constant. The values observed on current toolkits:

The first param_base bytes are reserved for driver-managed metadata (grid dimensions, block dimensions, shared memory size, etc.). User-specified kernel parameters are laid out contiguously starting at param_base, each aligned to its ABI alignment. For a kernel add(int a, int b), the two 4-byte parameters occupy offsets 0x0 and 0x4 relative to param_base. The code reads parameters with LDC (load constant) instructions:

LDC R0, c[0x0][param_base + 0x0]   // load 'a'
LDC R1, c[0x0][param_base + 0x4]   // load 'b'

The .nv.info metadata, the constant bank section, and the LDC instructions must all agree on the base offset. If they disagree, the driver copies launch arguments to one offset and the kernel reads from another: wrong results, no crash, no diagnostic.

.nv.info: the EIATTR metadata format

The .nv.info sections are the most opaque part of a cubin. They use SHT_LOPROC (0x70000000), a processor-specific section type. The content is a flat stream of EIATTR (ELF Info ATTRibute) entries. There is no public documentation for this format.

Each EIATTR entry has a fixed 4-byte header: byte 0 is the format (EIFMT), byte 1 is the attribute code (EIATTR), bytes 2-3 are the value or size depending on format. This is a TLV (type-length-value) scheme, except the "length" is implicit for formats 1-3 (always 4 bytes total) and explicit only for format 4.

The module-level .nv.info section contains attributes keyed by symbol index. For each kernel it emits three entries: REGCOUNT (0x2f), the number of general-purpose registers the kernel uses; FRAME_SIZE (0x11), the stack frame size in bytes; and MIN_STACK_SIZE (0x12), the minimum stack size. The driver uses regcount to compute occupancy, how many thread blocks can run concurrently on one SM. Over-reporting wastes occupancy. Under-reporting causes the hardware to clobber live registers.

The per-kernel section carries the kernel's ABI contract with the driver. The entries, in order:

Mental model

A cubin is a self-describing artifact: the machine code, the ABI contract, and the driver compatibility notes all live in one ELF file. The .nv.info metadata is the kernel's ID card, and the driver trusts it blindly. Get the register count wrong and the hardware clobbers live registers.

Putting it together

The complete flow from source to loaded kernel: .cu source → ptxas/llc → cubin (ELF64) → cuModuleLoad → driver reads sections → kernel launches. Every section has a job, and the driver's trust in the metadata is total.

For inference engineers, this matters because the cubin is the final artifact your kernels become. Understanding its structure is the difference between debugging a kernel launch failure by reading the ELF and guessing. And when you are building from-scratch emitters or custom tooling, the ABI version, the param_base convention, and the EIATTR format are the load-bearing details.

The cubin is a standard ELF file with an undocumented soul. The driver trusts its metadata completely, and the metadata must be exactly right.

Back to the blog