x86-64 asm 中的 .LX 例程标签是什么?

What are .LX routine tags for in x86-64 asm?

我试过在线搜索此内容,但没有找到任何答案。我正在研究汇编条件跳转并且正在使用这个 C 例程:

long absdiff (long x, long y) {
    long result;

    if (x > y)
        result = x-y;
    else
        result = y-x;

    return result;
}

我的笔记说它 return 是一个 asm 代码,类似于这个代码:

absdiff:
    cmpq %rsi, %rdi
    jle  .L4
    movq %rdi, %rax
    subq %rsi, %rax
    ret
.L4:
    movq %rsi, %rax
    subq %rdi, %rax
    ret

据我了解,如果 x <= y,例程将跳转到 .L4,然后 return 从该跳转到下一条指令并继续,直到 ret,我知道是错的。由于 %rax 写在 .L4 中,我认为它的 ret 适用于整个例程,而不是跳转到的例程,但我在调试 C 时也看到了更像这样的代码gdb 例程:

0x1119 <absdiff>     mov   %rdi,%rax
0x111c <absdiff+3>   cmp   %rsi,%rdi
0x111f <absdiff+6>   jle   0x1125 <absdiff+12>
0x1121 <absdiff+8>   sub   %rsi,%rax
0x1124 <absdiff+11>  retq
0x1125 <absdiff+12>  sub   %rdi,%rsi
0x1128 <absdiff+15>  mov   %rsi,%rax
0x112b <absdiff+18>  retq

在这里我理解例程return在不同点上就像你在C例程上写不同的return一样。所以我的问题是:.LX routine 标签在汇编语言中的含义是什么,它们与跳转到的例程有什么关系?

one:
    b .L77
    nop
    nop
.L77:
    b two
    nop
    nop
two:
    b .three
    nop
    nop
    nop
.three:
    nop
    nop
    


Disassembly of section .text:

00000000 <one>:
   0:   ea000001    b   c <one+0xc>
   4:   e1a00000    nop         ; (mov r0, r0)
   8:   e1a00000    nop         ; (mov r0, r0)
   c:   ea000001    b   18 <two>
  10:   e1a00000    nop         ; (mov r0, r0)
  14:   e1a00000    nop         ; (mov r0, r0)

00000018 <two>:
  18:   ea000002    b   28 <.three>
  1c:   e1a00000    nop         ; (mov r0, r0)
  20:   e1a00000    nop         ; (mov r0, r0)
  24:   e1a00000    nop         ; (mov r0, r0)

00000028 <.three>:
  28:   e1a00000    nop         ; (mov r0, r0)
  2c:   e1a00000    nop         ; (mov r0, r0)

编译器生成程序集,程序集被提供给汇编器并变成一个对象。编译器需要生成独立于您创建的标签(函数名称等)的标签,因此这个特定的标签使用 .Ln,其中 n 是一个数字,在汇编语言中它是唯一的 program/module/file.

此汇编程序显然保留了 binary/object 中的其他非 .Ln 标签,但丢弃了 .Ln 标签。然后你使用一个单独的工具,一个反汇编器,它选择它想要如何表示机器代码。在这种情况下,我们得到一个绝对地址 b c 表示 b 0xC 以及一个助手,0xC 位于距最近标签的偏移量 0xC 处。显然简单地在标签前面放一个点并不是让它消失的方法。

但是这个

one:
    b .L77
    nop
    nop
.L77:
    b two
    nop
    nop
two:
    b .Lthree
    nop
    nop
    nop
.Lthree:
    nop
    nop
    

00000000 <one>:
   0:   ea000001    b   c <one+0xc>
   4:   e1a00000    nop         ; (mov r0, r0)
   8:   e1a00000    nop         ; (mov r0, r0)
   c:   ea000001    b   18 <two>
  10:   e1a00000    nop         ; (mov r0, r0)
  14:   e1a00000    nop         ; (mov r0, r0)

00000018 <two>:
  18:   ea000002    b   28 <two+0x10>
  1c:   e1a00000    nop         ; (mov r0, r0)
  20:   e1a00000    nop         ; (mov r0, r0)
  24:   e1a00000    nop         ; (mov r0, r0)
  28:   e1a00000    nop         ; (mov r0, r0)
  2c:   e1a00000    nop         ; (mov r0, r0)

确实使它消失,所以人们会假设 .Lx 是一个有效的标签名称,但汇编程序不会将它放在输出二进制文件的符号 table 中。代码是正确的,它只是没有汇编语言所具有的所有标签,这很好,机器代码没有标签,它只是人类可读的东西。这种机制允许工具链轻松地为每个文件生成中间标签,而不必神奇地弄清楚如何避免冲突(这是不可能的)。

这个汇编器(family, gnu assembler, gas)也有编译器不使用但被一些懒惰的编码员使用的这个特性:

1:
    b 1f
    b 1b
    b 2f
1:
    nop
    nop
2:


00000000 <.text>:
   0:   ea000001    b   c <.text+0xc>
   4:   eafffffd    b   0 <.text>
   8:   ea000001    b   14 <.text+0x14>
   c:   e1a00000    nop         ; (mov r0, r0)
  10:   e1a00000    nop         ; (mov r0, r0)

1f 表示标签 1:在代码中向前 1b 表示在代码中向后标签 1(该方向的第一次出现)。您可以在整个代码中使用相同的标签名称 1: 或其中的一小部分 1: 2: 3: 以实现与 .Lx 相同的目的,但您甚至不必拥有唯一的标签。也许这适用于我没有尝试过的数字以外的东西。

.L4这样的标签名称由编译器自动编号,每次它需要一个分支目标时。

Clang 通过计算基本块来为其标签编号(因此第一个函数中的第 4 个基本块将具有类似 .LBB0_3 的标签名称),但我认为 GCC 仅在发出 (首先)跳转到那里的跳转指令。

这就是为什么标签本身在函数中不是严格按数字递增顺序排列,而是在文件中整体排列的原因。

GCC 从不跨越函数边界跳转到这些内部标签。


.Lname 标签是 local 标签,不进入目标文件 / executable 的符号 table。这就是为什么您在调试器中看不到它们,只看到函数名称的原因。

I asume its ret works for the whole routine, not the one jumped to,

是的。 ret 不是魔法。 ret 就是 pop %ripjne 不会推送 return 地址,因此它不是函数调用,只是一个普通的分支。

顺便说一句,一个函数有两种出路叫做"tail duplication"优化。他们不是让一条路径跳到另一条路径,而是只做任何清理工作和 ret。执行将通过其中之一,而不是两者。

but I've also seen this code more like this when debugging the C routine with gdb:

"but"?这就是您通过汇编 + linking 编译器生成的 asm.

得到的结果

符号命名的分支目标被替换为数字目标地址(在这种情况下由汇编器替换)。 (实际上编码为相对位移,如 jcc rel8。)汇编器能够在不等待 link 时间的情况下完成它,因为跳转与目标位于同一文件中,并且因为它是相对的。

jle指令执行跳转而不是调用。这会直接转移控制,而不会将 return 地址压入堆栈:它就像 C 中的 goto,而不是调用。这意味着下面的 ret returns 到 absdiff 的调用者,因为它仍然是堆栈上的顶部 return 地址。