既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
上段代码意思是,把fill_value变量中的值往edi指向的内存地址写入count次。也就是说,eax和edi中的内容不再有效。再来看下面的例子:
1
2
3
4
5
6
7
|
int a=10, b;
asm ("movl %1, %%eax; \n\t"
"movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);
|
这段代码意思是,把变量a的值赋值给b。
- ‘b’是输出操作符,%0引用它,并且’a’是输入操作符,1%引用它。
- ‘r’是操作符的限定符。这里’r’告诉GCC可以使用任意一个寄存器来存储操作符。’=’是输出操作符的限定符,并且是只写的。
- 在寄存器之前有两个’%’号。用来帮助GCC区别操作符还是寄存器。操作符只有一个’%’前缀。
换句话说,在扩展ASM语法中,如果在汇编中直接使用寄存器名字而不是通过%0 %1这样引用,则寄存器前需要两个%限定。 - 改动的寄存器%eax列在第三个冒号在后,告诉GCC %eax的值在汇编中有改动,所以GCC不会再用这个寄存器存储其他的值。
当asm结束时,’b’会反应更新过的数据,因为他被指定为输出操作符。换句话说,在汇编中改变’b’的值,会被反映到汇编之外。 - 但如果一个寄存器已经出现在输出操作符列表中,那么无需再将它添加到clobber list里,如果添加了编译的时候会报错。例如下面这段汇编是错误的:
1
2
3
4
5
6
7
|
int a=10, b;
asm ("movl %1, %%eax; \n\t"
"movl %%eax, %0;"
:"=b"(b) /* 明确指出使用寄存器ebx */
:"r"(a) /* input */
:"%eax", "%ebx" /* 编译出错!! */
);
|
编译器模板
操作数
每个 操作数都必须包含在“”之内。对于输出操作数,双引号(“”)中会多一个限定符,用来决定限定符地址的模式。
如果有多个操作数,他们使用逗号(,)隔开。
每个操作数都可以通过数字来引用,按照顺序一次命名。输入和输出操作数依次命名,第一个输出的操作数记作0,后面依次增加。
输出操作数必须是长类型,输入操作数没有这个限制。扩展汇编最常用来调用机器指令本身,跟编译器无关。如果输出表达式不是一个直接地址,例如一个位阈,限定符必须是寄存器。此事,GCC会使用寄存器作为内联汇编的输出,并且把寄存器的值存储到输出里。
综上,原始输出操作数必须是“只写”的;GCC假定在指令结束之前,数值都在这些操作数中,并且不需要生成。扩展汇编也支持读写操作数。
来看几个例子:
1
2
3
4
|
asm ("leal (%1, %1, 4), %0"
: "=r" (five_times_x)
: "r" (x)
);
|
这个例子中,输入是’x’,并且没有指定寄存器。GCC会自己选择一个。再同样给输出选择一个寄存器。如果我们想要输入输出使用同一个寄存器,可以告诉GCC我们希望那种读写的操作数,比如:
1
2
3
4
|
asm ("leal (%0, %0, 4), %0"
: "=r" (five_times_x)
: "0" (x)
);
|
此时,输入输出操作数会是同一个寄存器。但我们并不知道是具体那一个。如果想明确指定某一个寄存器,也有方法,比如:
1
2
3
4
|
asm ("leal (%%ecx,%%ecx,4), %%ecx"
: "=c" (x)
: "c" (x)
);
|
以上三个例子,并没有指定任何改动寄存器列表,为什么?前两个例子,GCC决定使用哪个寄存器,它会知道寄存器发生的变化。在最后一个例子中,我们也没有指定变化寄存器,因为GCC知道值最终保存到x中,在汇编之外,它知道ecx的值了,所以没有必要列出变化寄存器列表(clobber list),如果列上%ecx
编译就会错误。
变化寄存器列表(clobber)
有些指令会改变硬件寄存器,因此必须明确指出这些改动过的寄存器,将其列在第三个’:’之后。这是为了告诉GCC汇编使用并修改了那些寄存器。所以,GCC会知道之前被加载到这些寄存器的值已经无效了。同时没有必要列出放在输入和输出操作数中的寄存器,以内GCC知道内联已经使用了他们。需要明确指出的是那些没有明确指出的隐式使用的寄存器,那些没有列在输入和输出操作数中的寄存器。
如果指令修改了内存,需要在clobber list中添加”memory”。这样通知GCC内存缓存应该失效了。同时必须添加’volatile’关键字,如果内存修改并没有列在输入和输出操作数中时。
我们多次可以读写更改的寄存器,参考下面的例子,意思是调用子程序_foo,并且通过eax 和 ecx传递两个参数给他。
注:这里的寄存器名字前是否加%,eax
和%eax
都是正确的!
1
2
3
4
5
6
7
|
asm ("movl %0,%%eax; \n\t"
"movl %1,%%ecx; \n\t"
"call _foo"
: /* no outputs */
: "g" (from), "g" (to)
: "eax", "ecx"
);
|
Volatile 关键字
如果熟悉内核源码,我们会经常看到volatile或者volatile关键字在 asm或者asm之后。
如果内联汇编需要在它原来所在的位置处被执行,例如不被移到循环的外面或者不被优化掉,在asm 和 ()之间放一个volatile,像这样:
1
|
asm volatile ( ... : ... : ... : ...);
|
如果我们添加的汇编语言仅仅是为了计算并且没有任何边际效应,最好不要使用volatile关键字,因为volatile会妨碍代码优化。
关于限定符
前面的例子中我们已经使用了很多限定符,但还没有具体讲限定符的作用。限定符可以规定操作数是否在寄存器中,什么样的寄存器;以及操作数是指向内存以及内存地址类型;操作数是否是立即数,和数字的范围。来看下常用的限定符。
寄存器操作数限定符 ‘r’
当使用这个限定符时,操作数被存储在通用寄存器中(General Purpose Registers, GPR)。举例说明:
1
|
asm ("movl %%eax, %0\n" :"=r"(myval));
|
变量myval被存储到及粗糙那其中,寄存器eax中的值被copy到那个寄存器中,并且myval的值会被从寄存器更新到内存中。因为限定符’r’,gcc会把变量保存到任何一个通用寄存器中。如果需要明确指定某一个寄存器,需要使用相对应的限定符,如下表:
r | GPRs |
---|---|
a | %eax, %ax, %al |
b | %ebx, %bx, %bl |
c | %ecx, %cx, %cl |
d | %edx, %dx, %dl |
S | %esi, %si |
D | %edi, %di |
内存操作数限定符 ‘m’
当操作数在内存中,任何关于操作数的操作都直接访问内存地址。相反,寄存器操作数是先把数据存储到寄存器中,在写回到内存地址中。但寄存器限定符只有当指令明确需要或者明显加速处理,才会存储到寄存器中。当一个C变量需要在内联汇编更新的时候,内存限定符会更方便,并且,我们并不是真的需要用寄存器来存储值。例如存储IDTR的值到loc中:
1
|
asm("sidt %0\n" : :"m"(loc));
|
匹配限定符(Digit)
很多情况下,一个变量就可以做输入也可以做输出操作数,例如:
1
|
asm ("incl %0" :"=a"(var):"0"(var));
|
来看一个简单的例子,寄存器eax及用作输入同时用作输出操作数。变量var作为输入,传值给eax,并且在完成自增后,又更新到eax中。“0”这里指代同一个限定符,第0个,也就是输出变量。也就是输出变量var只会被存到eax中。通常下列情况可以这样使用:
- 当变量作为输入,并且协会到同一个变量中。
- 没必要把输入和输出分开的时候。
匹配限定符最重要的作用是高效的使用寄存器。
其他限定符:
“g” : Any register, memory or immediate integer operand is allowed, except for registers that are not general registers.
“m” : A memory operand is allowed, with any kind of address that the machine supports in general.
“o” : A memory operand is allowed, but only if the address is offsettable. ie, adding a small offset to the address gives a valid address.
“V” : A memory operand that is not offsettable. In other words, anything that would fit the m’ constraint but not the
o’constraint.
“i” : An immediate integer operand (one with constant value) is allowed. This includes symbolic constants whose values will be known only at assembly time.
“n” : An immediate integer operand with a known numeric value is allowed. Many systems cannot support assembly-time constants for operands less than a word wide. Constraints for these operands should use ’n’ rather than ’i’.
下面是X86特有的限定符:
“r” : Register operand constraint, look table given above.
“q” : Registers a, b, c or d.
“I” : Constant in range 0 to 31 (for 32-bit shifts).
“J” : Constant in range 0 to 63 (for 64-bit shifts).
“K” : 0xff.
“L” : 0xffff.
“M” : 0, 1, 2, or 3 (shifts for lea instruction).
“N” : Constant in range 0 to 255 (for out instruction).
“f” : Floating point register
“t” : First (top of stack) floating point register
“u” : Second floating point register
“A” : Specifies the ‘a’ or ‘d’ registers. This is primarily useful for 64-bit integer values intended to be returned with the ‘d’ register holding the most significant bits and the ‘a’ register holding the least significant bits.
限定修饰符
- ‘=’ 意思是操作数是“只写”的,之前的值会被输出值覆盖掉。
- ‘&’ 意思是输入操作数在本条指令完成之前,被修改。但这个操作数可能没有列在输入操作数列表或者是内存的一部分。如果输入仅仅用于早起结果的输入时,输入操作数可以被看做earlyclobber操作数
- ‘+’ 意思是操作数是“可读可写”的。
参考:https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
https://www.ibm.com/developerworks/cn/linux/sdk/assemble/inline/
http://flint.cs.yale.edu/cs421/papers/x86-asm/asm.html
实例
First we start with a simple example. We’ll write a program to add two numbers.
1
2
3
4
5
6
7
8
9
10
|
int main(void)
{
int foo = 10, bar = 15;
__asm__ __volatile__("addl %%ebx,%%eax"
:"=a"(foo)
:"a"(foo), "b"(bar)
);
printf("foo+bar=%d\n", foo);
return 0;
}
|
Here we insist GCC to store foo in %eax, bar in %ebx and we also want the result in %eax. The ’=’ sign shows that it is an output register. Now we can add an integer to a variable in some other way.
1
2
3
4
5
6
7
|
__asm__ __volatile__(
" lock ;\n"
" addl %1,%0 ;\n"
: "=m" (my_var)
: "ir" (my_int), "m" (my_var)
: /* no clobber-list */
);
|
This is an atomic addition. We can remove the instruction ’lock’ to remove the atomicity. In the output field, “=m” says that my_var is an output and it is in memory. Similarly, “ir” says that, my_int is an integer and should reside in some register (recall the table we saw above). No registers are in the clobber list.
Now we’ll perform some action on some registers/variables and compare the value.
1
2
3
4
5
|
__asm__ __volatile__( "decl %0; sete %1"
: "=m" (my_var), "=q" (cond)
: "m" (my_var)
: "memory"
);
|
Here, the value of my_var is decremented by one and if the resulting value is 0 then, the variable cond is set. We can add atomicity by adding an instruction “lock;\n\t” as the first instruction in assembler template.
In a similar way we can use “incl %0” instead of “decl %0”, so as to increment my_var.
Points to note here are that (i) my_var is a variable residing in memory. (ii) cond is in any of the registers eax, ebx, ecx and edx. The constraint “=q” guarantees it. (iii) And we can see that memory is there in the clobber list. ie, the code is changing the contents of memory.
How to set/clear a bit in a register? As next recipe, we are going to see it.
1
2
3
4
5
|
__asm__ __volatile__( "btsl %1,%0"
: "=m" (ADDR)
: "Ir" (pos)
: "cc"
);
|
Here, the bit at the position ’pos’ of variable at ADDR ( a memory variable ) is set to 1 We can use ’btrl’ for ’btsl’ to clear the bit. The constraint “Ir” of pos says that, pos is in a register, and it’s value ranges from 0-31 (x86 dependant constraint). ie, we can set/clear any bit from 0th to 31st of the variable at ADDR. As the condition codes will be changed, we are adding “cc” to clobberlist.
Now we look at some more complicated but useful function. String copy.
1
2
3
4
5
6
7
8
9
10
11
12
|
static inline char * strcpy(char * dest,const char *src)
{
int d0, d1, d2;
__asm__ __volatile__( "1:\tlodsb\n\t"
"stosb\n\t"
"testb %%al,%%al\n\t"
"jne 1b"
: "=&S" (d0), "=&D" (d1), "=&a" (d2)
: "0" (src),"1" (dest)
: "memory");
return dest;
}
|
The source address is stored in esi, destination in edi, and then starts the copy, when we reach at 0, copying is complete. Constraints “&S”, “&D”, “&a” say that the registers esi, edi and eax are early clobber registers, ie, their contents will change before the completion of the function. Here also it’s clear that why memory is in clobberlist.
We can see a similar function which moves a block of double words. Notice that the function is declared as a macro.
1
2
3
4
5
6
7
8
9
|
#define mov_blk(src, dest, numwords) \
__asm__ __volatile__ ( \
"cld\n\t" \
"rep\n\t" \
"movsl" \
: \
: "S" (src), "D" (dest), "c" (numwords) \
: "%ecx", "%esi", "%edi" \
)
|
Here we have no outputs, so the changes that happen to the contents of the registers ecx, esi and edi are side effects of the block movement. So we have to add them to the clobber list.
In Linux, system calls are implemented using GCC inline assembly. Let us look how a system call is implemented. All the system calls are written as macros (linux/unistd.h). For example, a system call with three arguments is defined as a macro as shown below.
1
2
3
4
5
6
7
8
9
![img](https://img-blog.csdnimg.cn/img_convert/38e4c87edb5de6bbf71085fbb9783925.png)
![img](https://img-blog.csdnimg.cn/img_convert/db46360a3bb5cbaeba040ff3a04d50f8.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
system calls are implemented using GCC inline assembly. Let us look how a system call is implemented. All the system calls are written as macros (linux/unistd.h). For example, a system call with three arguments is defined as a macro as shown below.
| | |
| --- | --- |
|
1
2
3
4
5
6
7
8
9
[外链图片转存中…(img-UAVQkjky-1715597266878)]
[外链图片转存中…(img-6aMCB9gU-1715597266878)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!