最牛X的GCC 内联汇编_早期修饰寄存器,已有千人收藏

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注运维)
img

正文

5. 扩展汇编

在基本内联汇编中,我们只有指令。然而在扩展汇编中,我们可以同时指定操作数。它允许我们指定输入寄存器、输出寄存器以及修饰寄存器列表。GCC 不强制用户必须指定使用的寄存器。我们可以把头疼的事留给 GCC ,这可能可以更好地适应 GCC 的优化。不管怎么说,基本格式为:
asm ( 汇编程序模板
: 输出操作数 /* 可选的 */
: 输入操作数 /* 可选的 */
: 修饰寄存器列表 /* 可选的 */
);

汇编程序模板由汇编指令组成。每一个操作数由一个操作数约束字符串所描述,其后紧接一个括弧括起的 C 表达式。冒号用于将汇编程序模板和第一个输出操作数分开,另一个(冒号)用于将最后一个输出操作数和第一个输入操作数分开(如果存在的话)。逗号用于分离每一个组内的操作数。总操作数的数目限制在 10 个,或者机器描述中的任何指令格式中的最大操作数数目,以较大者为准。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果没有输出操作数但存在输入操作数,你必须将两个连续的冒号放置于输出操作数原本会放置的地方周围。

示例:

asm ("cld/n/t"
     "rep/n/t"
     "stosl"
     : /* 无输出寄存器 */
     : "c" (count), "a" (fill_value), "D" (dest)
     : "%ecx", "%edi" 
     );

现在来看看这段代码是干什么的?以上的内联汇编是将 “fill_value” 值连续 “count” 次拷贝到寄存器 “edi” 所指位置(LCTT 译注:每执行 stosl 一次,寄存器 edi 的值会递增或递减,这取决于是否设置了 direction 标志,因此以上代码实则初始化一个内存块)。 它也告诉 gcc 寄存器 “ecx” 和 “edi” 一直无效。为了更加清晰地说明,让我们再看一个示例。

int a=10, b;
asm ("movl %1, %%eax; 
      movl %%eax, %0;"
     :"=r"(b)        /* 输出 */
     :"r"(a)         /* 输入 */
     :"%eax"         /* 修饰寄存器 */
     );       

这里我们所做的是使用汇编指令使 ’b’ 变量的值等于 ’a’ 变量的值。一些有意思的地方是:

  1. “b” 为输出操作数,用 %0 引用,并且 “a” 为输入操作数,用 %1 引用。
  2. “r” 为操作数约束。之后我们会更详细地了解约束(字符串)。目前,“r” 告诉 GCC 可以使用任一寄存器存储操作数。输出操作数约束应该有一个约束修饰符 “=” 。这修饰符表明它是一个只读的输出操作数。
  3. 寄存器名字以两个 % 为前缀。这有利于 GCC 区分操作数和寄存器。操作数以一个 % 为前缀。
  4. 第三个冒号之后的修饰寄存器 %eax 用于告诉 GCC %eax 的值将会在 “asm” 内部被修改,所以 GCC 将不会使用此寄存器存储任何其他值。

当 “asm” 执行完毕, “b” 变量会映射到更新的值,因为它被指定为输出操作数。换句话说, “asm” 内 “b” 变量的修改应该会被映射到 “asm” 外部。

现在,我们可以更详细地看看每一个域。

1.汇编程序模板

汇编程序模板包含了被插入到 C 程序的汇编指令集。其格式为:每条指令用双引号圈起,或者整个指令组用双引号圈起。同时每条指令应以分界符结尾。有效的分界符有换行符(“/n”)和分号(“;”)。“/n” 可以紧随一个制表符(“/t”)。我们应该都明白使用换行符或制表符的原因了吧(LCTT 译注:就是为了排版和分隔)?和 C 表达式对应的操作数使用 %0、%1 … 等等表示。

2.操作数

C 表达式用作 “asm” 内的汇编指令操作数。每个操作数前面是以双引号圈起的操作数约束。对于输出操作数,在引号内还有一个约束修饰符,其后紧随一个用于表示操作数的 C 表达式。即,“操作数约束”(C 表达式)是一个通用格式。对于输出操作数,还有一个额外的修饰符。约束字符串主要用于决定操作数的寻址方式,同时也用于指定使用的寄存器。

如果我们使用的操作数多于一个,那么每一个操作数用逗号隔开。

在汇编程序模板中,每个操作数用数字引用。编号方式如下。如果总共有 n 个操作数(包括输入和输出操作数),那么第一个输出操作数编号为 0 ,逐项递增,并且最后一个输入操作数编号为 n - 1 。操作数的最大数目在前一节我们讲过。

输出操作数表达式必须为左值。输入操作数的要求不像这样严格。它们可以为表达式。扩展汇编特性常常用于编译器所不知道的机器指令 😉。如果输出表达式无法直接寻址(即,它是一个位域),我们的约束字符串必须给定一个寄存器。在这种情况下,GCC 将会使用该寄存器作为汇编的输出,然后存储该寄存器的内容到输出。

正如前面所陈述的一样,普通的输出操作数必须为只写的; GCC 将会假设指令前的操作数值是死的,并且不需要被(提前)生成。扩展汇编也支持输入-输出或者读-写操作数。

所以现在我们来关注一些示例。我们想要求一个数的5次方结果。为了计算该值,我们使用 “lea” 指令。

asm ("leal (%1,%1,4), %0"
     : "=r" (five_times_x)
     : "r" (x) 
     );

这里我们的输入为 x。我们不指定使用的寄存器。 GCC 将会选择一些输入寄存器,一个输出寄存器,来做我们预期的工作。如果我们想要输入和输出放在同一个寄存器里,我们也可以要求 GCC 这样做。这里我们使用那些读-写操作数类型。这里我们通过指定合适的约束来实现它。

asm ("leal (%0,%0,4), %0"
     : "=r" (five_times_x)
     : "0" (x) 
     );

现在输出和输出操作数位于同一个寄存器。但是我们无法得知是哪一个寄存器。现在假如我们也想要指定操作数所在的寄存器,这里有一种方法。

asm ("leal (%%ecx,%%ecx,4), %%ecx"
     : "=c" (x)
     : "c" (x) 
     );

在以上三个示例中,我们并没有在修饰寄存器列表里添加任何寄存器,为什么?在头两个示例, GCC 决定了寄存器并且它知道发生了什么改变。在最后一个示例,我们不必将 ‘ecx’ 添加到修饰寄存器列表(LCTT 译注: 原文修饰寄存器列表这个单词拼写有错,这里已修正),gcc 知道它表示 x。因此,因为它可以知道 “ecx” 的值,它就不被当作修饰的(寄存器)了。

3.修饰寄存器列表

一些指令会破坏一些硬件寄存器内容。我们不得不在修饰寄存器中列出这些寄存器,即汇编函数内第三个 ’:’ 之后的域。这可以通知 gcc 我们将会自己使用和修改这些寄存器,这样 gcc 就不会假设存入这些寄存器的值是有效的。我们不用在这个列表里列出输入、输出寄存器。因为 gcc 知道 “asm” 使用了它们(因为它们被显式地指定为约束了)。如果指令隐式或显式地使用了任何其他寄存器,(并且寄存器没有出现在输出或者输出约束列表里),那么就需要在修饰寄存器列表中指定这些寄存器。

如果我们的指令可以修改条件码寄存器(cc),我们必须将 “cc” 添加进修饰寄存器列表。

如果我们的指令以不可预测的方式修改了内存,那么需要将 “memory” 添加进修饰寄存器列表。这可以使 GCC 不会在汇编指令间保持缓存于寄存器的内存值。如果被影响的内存不在汇编的输入或输出列表中,我们也必须添加 “volatile” 关键词。

我们可以按我们的需求多次读写修饰寄存器。参考一下模板内的多指令示例;它假设子例程 _foo 接受寄存器 “eax” 和 “ecx” 里的参数。

asm ("movl %0,%%eax;
      movl %1,%%ecx;
      call _foo"
     : /* no outputs */
     : "g" (from), "g" (to)
     : "eax", "ecx"
     );

4.Volatile …?

如果你熟悉内核源码或者类似漂亮的代码,你一定见过许多声明为 “volatile” 或者 "__volatile__"的函数,其跟着一个 “asm” 或者 “__asm__”。我之前提到过关键词 “asm” 和 “__asm__”。那么什么是 “volatile” 呢?

如果我们的汇编语句必须在我们放置它的地方执行(例如,不能为了优化而被移出循环语句),将关键词 “volatile” 放置在 asm 后面、()的前面。以防止它被移动、删除或者其他操作,我们将其声明为 “asm volatile ( … : … : … : …);”

如果担心发生冲突,请使用 “__volatile__”。

如果我们的汇编只是用于一些计算并且没有任何副作用,不使用 “volatile” 关键词会更好。不使用 “volatile” 可以帮助 gcc 优化代码并使代码更漂亮。

在“一些实用的诀窍”一节中,我提供了多个内联汇编函数的例子。那里我们可以了解到修饰寄存器列表的细节。

6. 更多关于约束

到这个时候,你可能已经了解到约束和内联汇编有很大的关联。但我们对约束讲的还不多。约束用于表明一个操作数是否可以位于寄存器和位于哪种寄存器;操作数是否可以为一个内存引用和哪种地址;操作数是否可以为一个立即数和它可能的取值范围(即值的范围),等等。

6.1 常用约束/strong>

在许多约束中,只有小部分是常用的。我们来看看这些约束。
1. 寄存器操作数约束

当使用这种约束指定操作数时,它们存储在通用寄存器(GPR)中。请看下面示例:

asm ("movl %%eax, %0/n" :"=r"(myval));

这里,变量 myval 保存在寄存器中,寄存器 eax 的值被复制到该寄存器中,并且 myval 的值从寄存器更新到了内存。当指定 “r” 约束时, gcc 可以将变量保存在任何可用的 GPR 中。要指定寄存器,你必须使用特定寄存器约束直接地指定寄存器的名字。它们为:

+---+--------------------+
| r |    Register(s)     |
+---+--------------------+
| a |   %eax, %ax, %al   |
| b |   %ebx, %bx, %bl   |
| c |   %ecx, %cx, %cl   |
| d |   %edx, %dx, %dl   |
| S |   %esi, %si        |
| D |   %edi, %di        |
+---+--------------------+

2. 内存操作数约束

当操作数位于内存时,任何对它们的操作将直接发生在内存位置,这与寄存器约束相反,后者首先将值存储在要修改的寄存器中,然后将它写回到内存位置。但寄存器约束通常用于一个指令必须使用它们或者它们可以大大提高处理速度的地方。当需要在 “asm” 内更新一个 C 变量,而又不想使用寄存器去保存它的值,使用内存最为有效。例如,IDTR 寄存器的值存储于内存位置 loc 处:

asm("sidt %0/n" : :"m"(loc));

3. 匹配(数字)约束

在某些情况下,一个变量可能既充当输入操作数,也充当输出操作数。可以通过使用匹配约束在 “asm” 中指定这种情况。

asm ("incl %0" :"=a"(var):"0"(var));

在操作数那一节中,我们也看到了一些类似的示例。在这个匹配约束的示例中,寄存器 “%eax” 既用作输入变量,也用作输出变量。 var 输入被读进 %eax,并且等递增后更新的 %eax 再次被存储进 var。这里的 “0” 用于指定与第 0 个输出变量相同的约束。也就是,它指定 var 输出实例应只被存储在 “%eax” 中。该约束可用于:

  • 在输入从变量读取或变量修改后且修改被写回同一变量的情况
  • 在不需要将输入操作数实例和输出操作数实例分开的情况

使用匹配约束最重要的意义在于它们可以有效地使用可用寄存器。

其他一些约束:

  1. “m” : 允许一个内存操作数,可以使用机器普遍支持的任一种地址。
  2. “o” : 允许一个内存操作数,但只有当地址是可偏移的。即,该地址加上一个小的偏移量可以得到一个有效地址。
  3. “V” : 一个不允许偏移的内存操作数。换言之,任何适合 “m” 约束而不适合 “o” 约束的操作数。
  4. “i” : 允许一个(带有常量)的立即整形操作数。这包括其值仅在汇编时期知道的符号常量。
  5. “n” : 允许一个带有已知数字的立即整形操作数。许多系统不支持汇编时期的常量,因为操作数少于一个字宽。对于此种操作数,约束应该使用 ‘n’ 而不是’i’。
  6. “g” : 允许任一寄存器、内存或者立即整形操作数,不包括通用寄存器之外的寄存器。

以下约束为 x86 特有。

  1. “r” : 寄存器操作数约束,查看上面给定的表格。
  2. “q” : 寄存器 a、b、c 或者 d。
  3. “I” : 范围从 0 到 31 的常量(对于 32 位移位)。
  4. “J” : 范围从 0 到 63 的常量(对于 64 位移位)。
  5. “K” : 0xff。
  6. “L” : 0xffff。
  7. “M” : 0、1、2 或 3 (lea 指令的移位)。
  8. “N” : 范围从 0 到 255 的常量(对于 out 指令)。
  9. “f” : 浮点寄存器
  10. “t” : 第一个(栈顶)浮点寄存器
  11. “u” : 第二个浮点寄存器
  12. “A” : 指定 “a” 或 “d” 寄存器。这主要用于想要返回 64 位整形数,使用 “d” 寄存器保存最高有效位和 “a” 寄存器保存最低有效位。

6.2 约束修饰符

当使用约束时,对于更精确的控制超过了对约束作用的需求,GCC 给我们提供了约束修饰符。最常用的约束修饰符为:

  1. “=” : 意味着对于这条指令,操作数为只写的;旧值会被忽略并被输出数据所替换。
  2. “&” : 意味着这个操作数为一个早期改动的操作数,其在该指令完成前通过使用输入操作数被修改了。因此,这个操作数不可以位于一个被用作输出操作数或任何内存地址部分的寄存器。如果在旧值被写入之前它仅用作输入而已,一个输入操作数可以为一个早期改动操作数。

上述的约束列表和解释并不完整。示例可以让我们对内联汇编的用途和用法更好的理解。在下一节,我们会看到一些示例,在那里我们会发现更多关于修饰寄存器列表的东西。

7. 一些实用的诀窍

现在我们已经介绍了关于 GCC 内联汇编的基础理论,现在我们将专注于一些简单的例子。将内联汇编函数写成宏的形式总是非常方便的。我们可以在 Linux 内核代码里看到许多汇编函数。(usr/src/linux/include/asm/*.h)。

  1. 首先我们从一个简单的例子入手。我们将写一个两个数相加的程序。
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;
}

这里我们要求 GCC 将 foo 存放于 %eax,将 bar 存放于 %ebx,同时我们也想要在 %eax 中存放结果。‘=’ 符号表示它是一个输出寄存器。现在我们可以以其他方式将一个整数加到一个变量。

__asm__ __volatile__(
                     "   lock       ;/n"
                     "   addl %1,%0 ;/n"
                     : "=m"  (my_var)
                     : "ir"  (my_int), "m" (my_var)
                     :                                 /* 无修饰寄存器列表 */
                     );

这是一个原子加法。为了移除原子性,我们可以移除指令 ‘lock’。在输出域中,“=m” 表明 myvar 是一个输出且位于内存。类似地,“ir” 表明 myint 是一个整型,并应该存在于其他寄存器(回想我们上面看到的表格)。没有寄存器位于修饰寄存器列表中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
2. 现在我们将在一些寄存器/变量上展示一些操作,并比较值。

__asm__ __volatile__(  "decl %0; sete %1"
                     : "=m" (my_var), "=q" (cond)
                     : "m" (my_var) 
                     : "memory"
                     );

这里,my_var 的值减 1 ,并且如果结果的值为 0,则变量 cond 置 1。我们可以通过将指令 “lock;/n/t” 添加为汇编模板的第一条指令以增加原子性。

以类似的方式,为了增加 my_var,我们可以使用 “incl %0” 而不是 “decl %0”。

这里需要注意的地方是(i)my_var 是一个存储于内存的变量。(ii)cond 位于寄存器 eax、ebx、ecx、edx 中的任何一个。约束 “=q” 保证了这一点。(iii)同时我们可以看到 memory 位于修饰寄存器列表中。也就是说,代码将改变内存中的内容。
3. 如何置 1 或清 0 寄存器中的一个比特位。作为下一个诀窍,我们将会看到它。

__asm__ __volatile__(   "btsl %1,%0"
                      : "=m" (ADDR)
                      : "Ir" (pos)
                      : "cc"
                      );

这里,ADDR 变量(一个内存变量)的 ‘pos’ 位置上的比特被设置为 1。我们可以使用 ‘btrl’ 来清除由 ‘btsl’ 设置的比特位。pos 的约束 “Ir” 表明 pos 位于寄存器,并且它的值为 0-31(x86 相关约束)。也就是说,我们可以设置/清除 ADDR 变量上第 0 到 31 位的任一比特位。因为条件码会被改变,所以我们将 “cc” 添加进修饰寄存器列表。
4. 现在我们看看一些更为复杂而有用的函数。字符串拷贝。

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;
}

源地址存放于 esi,目标地址存放于 edi,同时开始拷贝,当我们到达 0 时,拷贝完成。约束 “&S”、“&D”、“&a” 表明寄存器 esi、edi 和 eax 早期修饰寄存器,也就是说,它们的内容在函数完成前会被改变。这里很明显可以知道为什么 “memory” 会放在修饰寄存器列表。

我们可以看到一个类似的函数,它能移动双字块数据。注意函数被声明为一个宏。



**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注运维)**
![img](https://img-blog.csdnimg.cn/img_convert/3b22fe94d1557b163d72f91bbedb5a05.jpeg)

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
"&a" 表明寄存器 esi、edi 和 eax 早期修饰寄存器,也就是说,它们的内容在函数完成前会被改变。这里很明显可以知道为什么 "memory" 会放在修饰寄存器列表。

 我们可以看到一个类似的函数,它能移动双字块数据。注意函数被声明为一个宏。

 

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注运维)
[外链图片转存中…(img-DutHLnRK-1713252217001)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值