Programming · free software · the open webSubscribe via RSS

Why Is C Not Portable Assembly?

From the earlier reference collection. Read the current software blog.

C maps to machine code through an implementation’s rules, but it describes an abstract machine with its own types and semantics. Understanding that gap explains why the same source can behave differently across targets.

C describes behavior, not a fixed instruction set

A C program is written against the language’s abstract machine: objects have types, expressions have rules, and functions have defined interfaces. A compiler translates that model to a particular processor and operating environment. The result may resemble the instructions a programmer expects, but the source does not prescribe a one-to-one sequence of machine operations.

For example, x + y expresses integer addition according to C’s type and overflow rules. It does not name a particular processor’s add instruction, register arrangement, or calling convention. The compiler can choose among implementations as long as it preserves the behavior required by the language and the target’s implementation choices.

Some details belong to the implementation

The C standard leaves certain choices to each implementation. These are implementation-defined behaviors: the implementation must choose and document an answer. The width of an integer type such as int is one familiar example; its exact range can vary across implementations. Code that assumes a particular range without checking may work on one target and fail on another.

Other situations are unspecified or undefined, which are distinct categories. Unspecified behavior permits a choice without requiring that choice to be documented. Undefined behavior means the language imposes no requirements for that case. A programmer should not treat either category as a promise that a processor will do something predictable.

The target includes more than the CPU

Portability also depends on the platform around the processor. A program can rely on operating-system interfaces, file layouts, available libraries, or compiler extensions. These dependencies may be entirely reasonable, but they are not made portable merely because the code is written in C. A desktop application using a system-specific window API has a different portability story from a command-line program using only standard facilities.

Even low-level code can be portable when it expresses its assumptions carefully. If a protocol requires a 32-bit unsigned field, use a type and conversion strategy appropriate to that representation, and explicitly define the byte order on the wire. Do not assume that copying a native integer’s in-memory bytes creates a portable network format.

Treat C as a systems language with contracts

The useful question is not whether C is “close” to assembly, but which contract a piece of code follows: the C language rules, documented implementation behavior, or a target-specific convention. Make those boundaries visible in comments, build configuration, and tests on the intended platforms.

That approach preserves C’s ability to express efficient, low-level work while avoiding accidental dependence on one compiler or processor. When an optimization or hardware interaction requires a platform assumption, state it explicitly and isolate it behind a small interface.

Suggest a correction · All posts