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 %rip
。 jne
不会推送 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 地址。
我试过在线搜索此内容,但没有找到任何答案。我正在研究汇编条件跳转并且正在使用这个 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 %rip
。 jne
不会推送 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 地址。