C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
204: Inline Assembly Basics
One of the biggest lies you'll believe when starting with inline assembly is that the compiler treats the asm block as just another C statement. I've seen plenty of developers write a block of assembly, pass in a variable, modify that variable in a register, and then wonder why the C code behaves as if the assembly never happened.
The Myth: "The Compiler Sees What I'm Doing"
You might think that if you pass a variable into an assembly block, the compiler is "watching" the registers and will automatically update the C variable when the assembly finishes. Look at this snippet (written for GCC/Clang on x86_64):
int value = 10;
asm ("addl $1, %0" : : "r"(value));
// I'm adding 1 to value... right?
printf("%d", value);
In many cases, this will still print 10. Why? Because you told the compiler that value is an input, but you never told it that value is also an output. The compiler sees "r"(value) and thinks, "Okay, I'll put the current value of value into a register for the assembly block to use." But once the assembly block finishes, the compiler assumes the register it used is now garbage or unchanged; it doesn't automatically copy that register's value back into the memory location of value.
The Reality: Explicit Constraints and Clobbers
To make inline assembly actually work, you have to stop thinking like a C programmer and start thinking like a compiler. You aren't just writing code; you're writing a contract. You have to explicitly tell the compiler: "I am taking this value, I am changing it, and I want you to put the result back here."
The GCC-style asm block follows this pattern: asm ( code : outputs : inputs : clobbers );
If we want to actually increment that variable, we use the "+r" constraint. The + tells the compiler the operand is both read and written. Here is the corrected version:
int value = 10;
asm ("addl $1, %0"
: "+r"(value) // Output: read and write back to 'value'
: // No extra inputs
: // No clobbers
);
printf("%d", value); // Now it prints 11.
But there's a catch. Sometimes you use registers that aren't tied to a specific C variable—like the rax register for a return value or rcx for a loop counter. If you manually move data into rax inside your assembly block, the compiler has no idea you did that. It might be using rax to store a critical pointer for the rest of your C function. When your assembly block finishes, you've just nuked the compiler's internal state, and your program will crash in a way that is nearly impossible to debug.
This is where the "clobber list" comes in. The clobber list is your way of saying, "Hey, I'm messing with these specific registers; don't trust whatever was in them before."
int a = 5, b = 10;
int result;
asm ("movl %1, %%eax;"
"addl %2, %%eax;"
"movl %%eax, %0;"
: "=r"(result) // Output: result
: "r"(a), "r"(b) // Inputs: a and b
: "eax" // Clobber: I used eax, so don't rely on it!
);
I'll be honest: inline assembly is a double-edged sword. It's powerful for squeezing out every drop of performance or hitting a specific CPU instruction that C doesn't have a keyword for, but it breaks the compiler's ability to optimize your code. Use it sparingly. If you find yourself writing more than a few lines of assembly, you're better off writing a separate .s file and linking it normally.
📋 Practical Task
Exercise: Implementing a Hardware-Accelerated Population Count
The "population count" (popcnt) is an operation that counts the number of set bits (1s) in a binary integer. While you can do this with a loop in C, modern x86 CPUs have a dedicated popcnt instruction that does it in a single clock cycle.
Your task is to write a C function int count_set_bits(unsigned int n) that uses inline assembly to invoke the popcnt instruction.
Requirements:
- Use the
popcntinstruction (Note: the operand order forpopcntispopcnt source, destination). - Correctly define the input constraint for
n. - Correctly define the output constraint to return the count.
- Ensure you are using the correct register constraints so the compiler doesn't accidentally overwrite your data.
Test Case:
int result = count_set_bits(0b101101); // Binary for 45
// result should be 4
There are no comments for now.