In Linux code, it is common to see C code embedded with parts of assembly code that are either system-specific or performance-critical.

A long time ago, I had a terrible fear of inline assembly code, until I made up for the weaknesses in assembly.

You may have little to no experience with inline assembly code in your work, but once you get down to the bottom of the system or need to optimize for time-critical scenarios, your knowledge will make a difference!

In this article, we will take a closer look at how assembly language code can be embedded in C using asm keywords. The eight sample code examples range from simple to complex, and step by step introduce the key syntax rules of inline assembly.

I hope this article can become a stepping stone for you on the way to advanced master!

PS:

  1. The sample code uses AT&T assembly syntax on a Linux system;

  2. Article 8 example code, can reply [426] in the background of the public number, you can receive the download address;

Basic ASM format

The GCC compiler supports two forms of inline ASM code:

  1. Basic ASM format: operands are not supported.

  2. Extended ASM format: supports operands;

1. Grammar rules

Asm [volatile] (" assembly instruction ")

  1. All instructions must be enclosed in double quotation marks;

  2. If there is more than one instruction, it must be separated by \n separator. For typesetting, \t is usually added;

  3. Multiple assembly instructions, which can be written on one line or on multiple lines;

  4. The keyword asm can be replaced with asm;

  5. Volatile is optional. It is possible for the compiler to optimize assembly code. Using the volatile keyword tells the compiler not to optimize handwritten inline assembly code.

2. Test1. c insert null instruction

#include <stdio.h>
int main()
{
    asm ("nop");
    printf("hello\n");
    asm ("nop\n\tnop\n\t"
	 "nop");
    return 0;
}
Copy the code

Note: C automatically concatenates two consecutive string literals into one, so “nop\n\tnop\n\t” “nop” is automatically concatenated into one string.

Generate assembly code instructions:

gcc -m32 -S -o test1.s test1.c
Copy the code

Test1.s reads as follows (only the relevant parts of the inline assembly code are posted) :

#APP # 5 "test1.c" 1 nop # 0 "" 2 #NO_APP #APP # 7 "test1.c" 1 nop nop nop # 0 "" 2 #NO_APPCopy the code

As you can see, the inline assembly code is commented by two comments (#APP… #NO_APP) wrap. Two pieces of assembly code are embedded in the source code, so you can see that the assembly code generated by the GCC compiler contains both parts of the code.

Both parts of the embedded assembly code are null instruction NOP and have no meaning.

3. Test2.c operates on global variables

Assembly instructions are embedded in C code to calculate or perform certain functions. Let’s look at how to manipulate global variables in inline assembly instructions.

#include <stdio.h>

int a = 1;
int b = 2;
int c;

int main()
{
    asm volatile ("movl a, %eax\n\t"
        "addl b, %eax\n\t"
        "movl %eax, c");
    printf("c = %d \n", c);
    return 0;
}   
Copy the code

For the basics of the compiler in assembly instructions:

Eax and EBX are both 32-bit registers on the x86 platform. In the basic ASM format, the registers must be preceded by a percent sign %.

The 32-bit register Eax can be used as a 16-bit register (ax), or as an 8-bit register (ah, al). This article will only use the 32-bit register.

Code description:

Movl a, %eax // copy the value of variable a into the %eax register;

Addl b, %eax // add the value of b to the value (a) in the %eax register;

Movl %eax, c // copy the value in the %eax register to the variable c;

Generate assembly code instructions:

gcc -m32 -S -o test2.s test2.c
Copy the code

Test2.s reads as follows (only those relevant to inline assembly code are posted) :

#APP
# 9 "test2.c" 1
	movl a, %eax
	addl b, %eax
	movl %eax, c
# 0 "" 2
#NO_APP
Copy the code

As you can see, in inline assembly code, you can directly operate with the names of global variables a and b. Execute test2 to get the correct result.

Consider this question: Why is it possible to use variables A, b, and c in assembly code?

Looking before the inline assembly code in test2.s, you can see:

.file "test2.c" .globl a .data .align 4 .type a, @object .size a, 4 a: .long 1. globl b. align 4. type b, @object.size b, 4 b:.long 2. comm c,4 4Copy the code

The variables a, b, and c are decorated with.globl and.comm, so they are exported globally and can be used in assembly code.

So here’s the question: if it’s a local variable, it won’t be exported with.globl in assembly code. Can it be used directly in inline assembly instructions?

Seeing is believing. Let’s try these three variables as local variables inside main.

4. Test3.c tries to manipulate local variables

#include <stdio.h>
int main()
{
    int a = 1;
    int b = 2;
    int c;

    asm("movl a, %eax\n\t"
        "addl b, %eax\n\t"
        "movl %eax, c");
    printf("c = %d \n", c);
    return 0;
}
Copy the code

Generate assembly code instructions:

gcc -m32 -S -o test3.s test3.c
Copy the code

In test3.s, you can see that there are no exported symbols for a, b, and c. A and b are not used elsewhere, so their values are copied directly into the stack space:

movl	$1, -20(%ebp)
movl	$2, -16(%ebp)
Copy the code

Let’s try compiling into an executable program:

$ gcc -m32 -o test3 test3.c
/tmp/ccuY0TOB.o: In function `main':
test3.c:(.text+0x20): undefined reference to `a'
test3.c:(.text+0x26): undefined reference to `b'
test3.c:(.text+0x2b): undefined reference to `c'
collect2: error: ld returned 1 exit status
Copy the code

Error: cannot find references to a,b,c! So what do you do to use local variables? Extend the ASM format!

Expand the ASM format

1. Instruction format

Asm [volatile] (” assembly instruction “:” output operand list “:” input operand list “:” Changed register “)

Format specification

  1. Assembly instructions: same as the basic ASM format;

  2. Output list of operands: how assembly code passes the result of processing to C code;

  3. Input operand list: how C code passes data to inline assembly code;

  4. Modified registers: Tells the compiler which registers are being used in inline assembly code.

  5. “Changed register” can be omitted, in which case the last colon may be omitted, but the preceding colon must be retained even if the output/input operand list is empty.

One more explanation of “changed registers” : GCC uses a series of registers when compiling C code; Some registers are also used in our handwritten inline assembly code.

To inform the compiler of which registers are being used by our users in inline assembly code, list them here so that GCC will avoid using them

2. Format of the output and input operand lists

In a system, there are only two places to store variables: registers and memory. Therefore, telling inline assembly code the output and input operands is telling it:

  1. Which registers or memory addresses to output results to;

  2. Which registers or memory addresses to read the input data from;

This process also has to meet a certain format:

"[output modifier] constraint "(register or memory address)Copy the code

(1) Constraints

It tells the compiler which registers to use, or which memory addresses to use, by different characters. These include the following characters:

A: Use the EAX/AX/AL register;

B: Use the EBX/BX/BL register;

C: use ecX/Cx/CL registers;

D: Use the EDX /dx/ DL register;

R: Use any available general-purpose register;

M: use the memory location of the variable;

The other constraint options are D, S, q, A, F, T, u, and so on. Check the documentation when you need it.

(2) Output modifier

As the name suggests, it is used to decorate the output, providing additional information about the output register or memory address, including the following four modifiers:

  1. + : The modified operand can be read or written;

  2. = : The modified operand can only be written;

  3. % : the modified operand can be interchanged with the next operand;

  4. & : The modified operands can be deleted or reused before the inline function is complete;

Language description is more abstract, see example directly!

3. Test4.c Operates local variables through registers

#include <stdio.h>

int main()
{
    int data1 = 1;
    int data2 = 2;
    int data3;

    asm("movl %%ebx, %%eax\n\t"
        "addl %%ecx, %%eax"
        : "=a"(data3)
        : "b"(data1),"c"(data2));

    printf("data3 = %d \n", data3);
    return 0;
}
Copy the code

There are two things to pay attention to:

  1. In inline assembly code, the list of “changed registers” is not declared, which means it can be omitted (and the preceding colon is not required);

  2. In the extended ASM format, the register must be preceded by 2 %;

Explanation of code:

  1. “B “(data1),”c”(data2) ==> Copy variable data1 to register %ebx, variable data2 to register %ecx. In this way, in inline assembly code, the two registers can be used to manipulate the two numbers;

  2. “=a”(data3) ==> Put the result in register %eax and copy it to data3. The preceding equals modifier means that data will be written to %eax, but not read from it;

With this format, you can use the specified register to manipulate local variables in inline assembly code. You’ll see how local variables are copied from the stack space into the register later.

Generate assembly code instructions:

gcc -m32 -S -o test4.s test4.c
Copy the code

Assembly code test4.s looks like this:

	movl	$1, -20(%ebp)
	movl	$2, -16(%ebp)
	movl	-20(%ebp), %eax
	movl	-16(%ebp), %edx
	movl	%eax, %ebx
	movl	%edx, %ecx
#APP
# 10 "test4.c" 1
	movl %ebx, %eax
	addl %ecx, %eax
# 0 "" 2
#NO_APP
    movl	%eax, -12(%ebp)
Copy the code

As you can see, before going into the handwritten inline assembly code:

  1. Copy the number 1 through the stack space (-20(%ebp)) to register %eax and then to register %ebx;

  2. Copy the number 2 through the stack space (-16(%ebp)), to register %edx, and then to register %ecx;

These two operations correspond to the “input operand list” section of inline assembly code: “b”(data1),”c”(data2).

After inlined assembly code (after #NO_APP), copy the value in the %eax register to the -12(%ebp) position on the stack, where the local variable data3 is, and the output operation is complete.

4. Test5.c declares the changed register

In test4.c, we do not declare the changed registers, so the compiler can choose which registers to use. As you can see from the generated assembly code test4.s, GCC uses the %edx register.

So let’s test this: tell GCC not to use the %edx register.

#include <stdio.h>
int main()
{
    int data1 = 1;
    int data2 = 2;
    int data3;

    asm("movl %%ebx, %%eax\n\t"
        "addl %%ecx, %%eax"
        : "=a"(data3)
        : "b"(data1),"c"(data2)
        : "%edx");

    printf("data3 = %d \n", data3);
    return 0;
}
Copy the code

In this code, the “%edx” at the end of the ASM instruction is used to tell the GCC compiler that we will use the %edx register in inline assembly code, so you should not use it.

Generate assembly code instructions:

gcc -m32 -S -o test5.s test5.c
Copy the code

Take a look at the generated assembly code test5.s:

    movl	$1, -20(%ebp)
	movl	$2, -16(%ebp)
	movl	-20(%ebp), %eax
	movl	-16(%ebp), %ecx
	movl	%eax, %ebx
#APP
# 10 "test5.c" 1
	movl %ebx, %eax
	addl %ecx, %eax
# 0 "" 2
#NO_APP
	movl	%eax, -12(%ebp)
Copy the code

As you can see, GCC did not choose to use the register %edx before inlining the assembly code.

Use placeholders instead of register names

In the example above, only two registers are used to operate on two local variables, and with many operands, it would be inconvenient to write the name of each register in inline assembly code.

Therefore, extending the ASM format gives us another lazy way to use registers in the output and input operand lists: placeholders!

Placeholders are a bit similar to batch scripts that use 1,1, 1,2… To refer to input parameters, inline assembly code placeholders, starting with registers in the output operand list and numbering from 0 to all registers in the input operand list.

Still see example more direct!

1. Test6.c uses placeholders instead of registers

#include <stdio.h>
int main()
{
    int data1 = 1;
    int data2 = 2;
    int data3;

    asm("addl %1, %2\n\t"
        "movl %2, %0"
        : "=r"(data3)
        : "r"(data1),"r"(data2));

    printf("data3 = %d \n", data3);
    return 0;
}
Copy the code

Code description:

  1. Output operand list “=r”(data3) : the constraint uses the character r, that is, no register is specified, and the compiler chooses which register to use to store the result, which is copied to the local variable data3;

  2. Input operand list “r”(data1),”r”(data2) : constraint character r, no register is specified, it is up to the compiler to choose which two registers to use to receive the local variables data1 and data2;

  3. Only one register is needed in the list of output operands, so %0 in inline assembly code represents this register (that is, counting from 0).

  4. There are two registers in the input operand list, so %1 and %2 represent them in inline assembly code (i.e., counting sequentially from the last register in the output operand list);

Generate assembly code instructions:

gcc -m32 -S -o test6.s test6.c
Copy the code

The assembly code is as follows test6.s:

	movl	$1, -20(%ebp)
	movl	$2, -16(%ebp)
	movl	-20(%ebp), %eax
	movl	-16(%ebp), %edx
#APP
# 10 "test6.c" 1
	addl %eax, %edx
	movl %edx, %eax
# 0 "" 2
#NO_APP
	movl	%eax, -12(%ebp)
Copy the code

As you can see, the GCC compiler selects %eax to store the local variable data1, %edx to store the local variable data2, and the result of the operation is also stored in the %eax register.

Does it feel easier to operate this way? Instead of us specifying which registers to use, it’s up to the compiler to choose.

In inline assembly code, registers are used with placeholders such as %0, %1, and %2.

Don’t worry, if you find using numbers cumbersome and error-prone, there is another, more convenient operation: Extending the ASM format also allows you to rename these placeholders, which is to give each register an alias and then use the alias to manipulate the register in inline assembly code.

Still looking at the code!

Test7.c alias registers

#include <stdio.h>
int main()
{
    int data1 = 1;
    int data2 = 2;
    int data3;

    asm("addl %[v1], %[v2]\n\t"
        "movl %[v2], %[v3]"
        : [v3]"=r"(data3)
        : [v1]"r"(data1),[v2]"r"(data2));

    printf("data3 = %d \n", data3);
    return 0;
}
Copy the code

Code description:

  1. Output list of operands: alias v3 is given to the register (chosen by the GCC compiler);

  2. Input operand list: aliases v1 and v2 are given to registers (selected by the GCC compiler);

Once the aliases are up, the aliases (%[v1], %[v2], %[v3]) can be used directly to manipulate data in inline assembly code.

Generate assembly code instructions:

gcc -m32 -S -o test7.s test7.c
Copy the code

Take a look at the generated assembly code test7.s:

	movl	$1, -20(%ebp)
	movl	$2, -16(%ebp)
	movl	-20(%ebp), %eax
	movl	-16(%ebp), %edx
#APP
# 10 "test7.c" 1
	addl %eax, %edx
	movl %edx, %eax
# 0 "" 2
#NO_APP
	movl	%eax, -12(%ebp)
Copy the code

This part of the assembly code is exactly the same as in Test6.s!

Use memory addresses

In the above example, registers are used for both the output and input operand lists (constraint characters: A, b, C, D, r, and so on).

We can specify which registers to use, and we can leave it to the compiler to choose which registers to use. It’s faster to manipulate data through registers.

If we wish, we can manipulate the variable directly using its memory address, in which case we need to use the constraint character M.

Test8.c uses memory addresses to manipulate data

#include <stdio.h>
int main()
{
    int data1 = 1;
    int data2 = 2;
    int data3;

    asm("movl %1, %%eax\n\t"
        "addl %2, %%eax\n\t"
        "movl %%eax, %0"
        : "=m"(data3)
        : "m"(data1),"m"(data2));

    printf("data3 = %d \n", data3);
    return 0;
}
Copy the code

Code description:

  1. Output the list of operands “=m”(data3) : use the memory address of the variable data3 directly;

  2. Enter the operand list “m”(data1),”m”(data2) : use the memory address of the variables data1, data2 directly;

In inline assembly code, you need to use a register (%eax) because you need to do the add calculation, and you definitely need a register for the calculation.

When manipulating the data in those memory addresses, sequentially numbered placeholders are still used.

Generate assembly code instructions:

gcc -m32 -S -o test8.s test8.c
Copy the code

The generated assembly code is test8.s:

	movl	$1, -24(%ebp)
	movl	$2, -20(%ebp)
#APP
# 10 "test8.c" 1
	movl -24(%ebp), %eax
	addl -20(%ebp), %eax
	movl %eax, -16(%ebp)
# 0 "" 2
#NO_APP
	movl	-16(%ebp), %eax
Copy the code

As you can see, before entering the inline assembly code, the values of datA1 and data2 are placed on the stack, and then the stack data is directly operated with the register %eax. Finally, the result of the operation (%eax) is copied to data3 on the stack (-16(%ebp)).

Five, the summary

With the above eight examples, we have covered the key syntax rules in inline assembly code, and with this foundation, you can write more complex instructions in inline assembly code.

Hope the above content can be helpful to you! Thank you very much!

Article 8 example code, you can reply [426] in the background of the public number, you can receive the download address.

















1. C Pointers – from basic principles to fancy tricks, illustrated in graphics and code. The original GDB bottom debugging principle so simple 3 step by step analysis – how to use C object-oriented programming 4. How to use Google protobuf to think, design and implement their own RPC framework 5. It is said that software architecture should be layered and divided into modules. What should be done specifically? (I)