最牛X的GCC 内联汇编_早期修饰寄存器(1),2024年最新Linux运维程序员如何有效提升学习效率

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

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

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

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

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

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

正文

Intel 语法中的 “Op-code dst src” 变为 AT&T 语法中的 “Op-code src dst”。

2.寄存器命名

寄存器名称有 “%” 前缀,即如果必须使用 “eax”,它应该用作 “%eax”。

3.立即数

AT&T 立即数以 “ " 为前缀。静态 " C " 变量也使用 " " 为前缀。静态 "C" 变量也使用 " "为前缀。静态"C"变量也使用"” 前缀。在 Intel 语法中,十六进制常量以 “h” 为后缀,然而 AT&T 不使用这种语法,这里我们给常量添加前缀 “0x”。所以,对于十六进制,我们首先看到一个 “$”,然后是 “0x”,最后才是常量。

4.操作数大小

在 AT&T 语法中,存储器操作数的大小取决于操作码名字的最后一个字符。操作码后缀 ’b’ 、’w’、’l’ 分别指明了字节(8位)、字(16位)、长型(32位)存储器引用。Intel 语法通过给存储器操作数添加 “byte ptr”、 “word ptr” 和 “dword ptr” 前缀来实现这一功能。

因此,Intel的 “mov al, byte ptr foo” 在 AT&T 语法中为 “movb foo, %al”。

5.存储器操作数

在 Intel 语法中,基址寄存器包含在 “[” 和 “]” 中,然而在 AT&T 中,它们变为 “(” 和 “)”。另外,在 Intel 语法中, 间接内存引用为

“section:[base + index*scale + disp]”,在 AT&T中变为 “section:disp(base, index, scale)”。

需要牢记的一点是,当一个常量用于 disp 或 scale,不能添加 “$” 前缀。

现在我们看到了 Intel 语法和 AT&T 语法之间的一些主要差别。我仅仅写了它们差别的一部分而已。关于更完整的信息,请参考 GNU 汇编文档。现在为了更好地理解,我们可以看一些示例。

+------------------------------+------------------------------------+
|       Intel Code             |      AT&T Code                     |
+------------------------------+------------------------------------+
| mov     eax,1                |  movl    $1,%eax                   |   
| mov     ebx,0ffh             |  movl    $0xff,%ebx                |   
| int     80h                  |  int     $0x80                     |   
| mov     ebx, eax             |  movl    %eax, %ebx                |
| mov     eax,[ecx]            |  movl    (%ecx),%eax               |
| mov     eax,[ebx+3]          |  movl    3(%ebx),%eax              | 
| mov     eax,[ebx+20h]        |  movl    0x20(%ebx),%eax           |
| add     eax,[ebx+ecx*2h]     |  addl    (%ebx,%ecx,0x2),%eax      |
| lea     eax,[ebx+ecx]        |  leal    (%ebx,%ecx),%eax          |
| sub     eax,[ebx+ecx*4h-20h] |  subl    -0x20(%ebx,%ecx,0x4),%eax |
+------------------------------+------------------------------------+

4. 基本内联

基本内联汇编的格式非常直接了当。它的基本格式为:
asm(“汇编代码”);
示例

asm("movl %ecx %eax"); /* 将 ecx 寄存器的内容移至 eax  */
__asm__("movb %bh (%eax)"); /* 将 bh 的一个字节数据 移至 eax 寄存器指向的内存 */

你可能注意到了这里我使用了 “asm” 和 “__asm__”。这两者都是有效的。如果关键词 “asm” 和我们程序的一些标识符冲突了,我们可以使用 “__asm__”。如果我们的指令多于一条,我们可以每个一行,并用双引号圈起,同时为每条指令添加 ’/n’ 和 ’/t’ 后缀。这是因为 gcc 将每一条当作字符串发送给as(GAS)(LCTT 译注: GAS 即 GNU 汇编器),并且通过使用换行符/制表符发送正确格式化后的行给汇编器。

示例

__asm__ ("movl %eax, %ebx/n/t"
         "movl $56, %esi/n/t"
         "movl %ecx, $label(%edx,%ebx,$4)/n/t"
         "movb %ah, (%ebx)");

如果在代码中,我们涉及到一些寄存器(即改变其内容),但在没有恢复这些变化的情况下从汇编中返回,这将会导致一些意想不到的事情。这是因为 GCC 并不知道寄存器内容的变化,这会导致问题,特别是当编译器做了某些优化。在没有告知 GCC 的情况下,它将会假设一些寄存器存储了一些值——而我们可能已经改变却没有告知 GCC——它会像什么事都没发生一样继续运行(LCTT 译注:什么事都没发生一样是指GCC不会假设寄存器装入的值是有效的,当退出改变了寄存器值的内联汇编后,寄存器的值不会保存到相应的变量或内存空间)。我们所可以做的是使用那些没有副作用的指令,或者当我们退出时恢复这些寄存器,要不就等着程序崩溃吧。这是为什么我们需要一些扩展功能,扩展汇编给我们提供了那些功能。

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 给我们提供了约束修饰符。最常用的约束修饰符为:

为了做好运维面试路上的助攻手,特整理了上百道 【运维技术栈面试题集锦】 ,让你面试不慌心不跳,高薪offer怀里抱!

这次整理的面试题,小到shell、MySQL,大到K8s等云原生技术栈,不仅适合运维新人入行面试需要,还适用于想提升进阶跳槽加薪的运维朋友。

本份面试集锦涵盖了

  • 174 道运维工程师面试题
  • 128道k8s面试题
  • 108道shell脚本面试题
  • 200道Linux面试题
  • 51道docker面试题
  • 35道Jenkis面试题
  • 78道MongoDB面试题
  • 17道ansible面试题
  • 60道dubbo面试题
  • 53道kafka面试
  • 18道mysql面试题
  • 40道nginx面试题
  • 77道redis面试题
  • 28道zookeeper

总计 1000+ 道面试题, 内容 又全含金量又高

  • 174道运维工程师面试题

1、什么是运维?

2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?

3、现在给你三百台服务器,你怎么对他们进行管理?

4、简述raid0 raid1raid5二种工作模式的工作原理及特点

5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?

6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?

7、Tomcat和Resin有什么区别,工作中你怎么选择?

8、什么是中间件?什么是jdk?

9、讲述一下Tomcat8005、8009、8080三个端口的含义?

10、什么叫CDN?

11、什么叫网站灰度发布?

12、简述DNS进行域名解析的过程?

13、RabbitMQ是什么东西?

14、讲一下Keepalived的工作原理?

15、讲述一下LVS三种模式的工作过程?

16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?

17、如何重置mysql root密码?

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

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注运维)
img

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

11、什么叫网站灰度发布?

12、简述DNS进行域名解析的过程?

13、RabbitMQ是什么东西?

14、讲一下Keepalived的工作原理?

15、讲述一下LVS三种模式的工作过程?

16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?

17、如何重置mysql root密码?

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

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

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

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值