static int func_name (const uint8_t * address) { int result; asm ("movl $1f, %0; movzbl %1, %0; 1:" : "=&a" (result) : "m" (*address)); return result; }
我已经浏览了互联网上的内联汇编引用。但是我无法弄清楚这段代码在做什么,例如,什么是$1f?“m”是什么意思?正常的内联约定不是使用“=r”和“r”吗?
des4xlb01#
该代码在功能上与return *address相同,但并不绝对等效于此生成的二进制/目标文件。在ELF中,* 前向引用 * 的用法(例如,mov $1f, ...撷取组件区域标签的位址)会导致建立所谓的 * 重新配置 *。重新配置是 * 连接器 * 的指令(在创建可执行文件时或稍后在加载可执行文件/库时插入到动态链接器中),以 * 插入 * 一个仅在链接/加载时才知道的值。在目标代码中,如下所示:
return *address
mov $1f, ...
Disassembly of section .text: 0000000000000000 : 0: b8 00 00 00 00 mov $0x0,%eax 5: 0f b6 07 movzbl (%rdi),%eax 8: c3 retq
请注意,这里的值(.text部分中的偏移量1)是零,尽管这实际上是不正确的--它取决于函数在运行代码中的结束位置。只有(动态)链接器最终才能知道这一点,而这段内存在加载时需要更新的信息实际上被放入了目标文件中:
.text
$ readelf -a xqf.o ELF Header: [ ... ] Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 0000000000000009 0000000000000000 AX 0 0 16 [ 2] .rela.text RELA 0000000000000000 000004e0 0000000000000018 0000000000000018 10 1 8 [ ... ] Relocation section '.rela.text' at offset 0x4e0 contains 1 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000001 00020000000a R_X86_64_32 0000000000000000 .text + 8 [ ... ]
此ELF部分条目表示:
1
R_X86_64_32
R_X86_64_PC32
lea 1f(%rip), %0
.text + 8
这个条目是由mov $1f, %0指令创建的,如果你不写这个条目(或者只写return *address),它就不会在那里。我通过删除static限定符强制生成上述代码;如果不这样做,一个简单的编译实际上 * 根本不会创建任何代码 *(如果不使用static代码,则会将其删除;如果使用了static代码,则会将其内联)。由于函数是static,正如前面所说,编译器通常会在调用点内联它。因此,使用它的信息通常会丢失,调试器检测它的能力也会丢失。但这里显示的技巧可以恢复这一点(间接),因为每次使用该函数都会创建 * 一个重定位入口 *。除此之外,类似这样的方法可以用于在二进制文件中建立 * 插桩点 *;在可通过目标文件格式恢复的位置处插入公知的/严格定义的但功能上无意义的小汇编语句,然后在需要时让例如调试器/跟踪实用程序用“更有用”的东西来替换它们。
mov $1f, %0
static
x33g5p2x2#
$1f是1标签的地址。f指定正向查找名为1的第一个标签。"m"是内存中的输入操作数。"=&a"是使用eax寄存器的输出操作数。a指定要使用的寄存器,=使其成为输出操作数。并且&保证其它操作数将不共享同一寄存器。这里,%0将被第一个操作数(eax寄存器)替换,%1将被第二个操作数(address指向的地址)替换。GCC文档中关于Inline assembly和asm约束的部分对所有这些以及更多内容进行了解释。
$1f
f
"m"
"=&a"
eax
a
=
&
%0
%1
address
h7wcgrx33#
这段代码(除了由于两个错别字而不可编译之外)几乎没有什么用处。这是它的转换结果(使用-S开关):
-S
_func_name: movl 4(%esp), %edx ; edx = the "address" parameter movl $1f, %eax ; eax = the address of the "1" label movzbl (%edx), %eax; eax = byte from address in edx, IOW, "*address" 1: ret
所以整个函数体可以用
return *address;
tez616oj4#
这是来自PintOS项目的代码片段。这里的函数由操作系统内核使用,用于从用户地址空间**读取address**处的字节。这是由movzbl %1, %0完成的,其中0%为result,1%为address。但在此之前,内核必须移动$1f的地址(movzbl %1, %0后面的指令地址)添加到eax寄存器。这个动作看起来没有用,因为一些上下文信息丢失了。内核这样做是为了让页面错误中断处理程序使用它。因为address可能是用户提供的无效的,它可能会导致页面错误。当这种情况发生时,中断处理程序会接管。将eip设置为等于eax(这是1f%的内存地址),并且还将eax设置为-1以指示读取失败。在此之后,内核能够从处理程序返回到$1f并继续前进。如果不保存$1f的地址,处理程序将不知道它应该返回到哪里,并且只能一次又一次地返回到movzbl %1, %0。
movzbl %1, %0
0%
result
1%
eip
1f%
-1
4条答案
按热度按时间des4xlb01#
该代码在功能上与
return *address
相同,但并不绝对等效于此生成的二进制/目标文件。在ELF中,* 前向引用 * 的用法(例如,
mov $1f, ...
撷取组件区域标签的位址)会导致建立所谓的 * 重新配置 *。重新配置是 * 连接器 * 的指令(在创建可执行文件时或稍后在加载可执行文件/库时插入到动态链接器中),以 * 插入 * 一个仅在链接/加载时才知道的值。在目标代码中,如下所示:请注意,这里的值(
.text
部分中的偏移量1)是零,尽管这实际上是不正确的--它取决于函数在运行代码中的结束位置。只有(动态)链接器最终才能知道这一点,而这段内存在加载时需要更新的信息实际上被放入了目标文件中:此ELF部分条目表示:
1
到.text
部分R_X86_64_32
)。这可能是为了在32位代码中使用,但在64位非PIE可执行文件中,这仍然是将地址放入寄存器的最有效的方法;对于R_X86_64_PC32
RIP相关的重定位,小于lea 1f(%rip), %0
。是的,将RIP相关的莱亚写入32位寄存器是法律的的,并且如果您不关心截断地址,则可以节省一个字节的机器码。.text + 8
的值(必须在链接/加载时计算)这个条目是由
mov $1f, %0
指令创建的,如果你不写这个条目(或者只写return *address
),它就不会在那里。我通过删除
static
限定符强制生成上述代码;如果不这样做,一个简单的编译实际上 * 根本不会创建任何代码 *(如果不使用static
代码,则会将其删除;如果使用了static
代码,则会将其内联)。由于函数是
static
,正如前面所说,编译器通常会在调用点内联它。因此,使用它的信息通常会丢失,调试器检测它的能力也会丢失。但这里显示的技巧可以恢复这一点(间接),因为每次使用该函数都会创建 * 一个重定位入口 *。除此之外,类似这样的方法可以用于在二进制文件中建立 * 插桩点 *;在可通过目标文件格式恢复的位置处插入公知的/严格定义的但功能上无意义的小汇编语句,然后在需要时让例如调试器/跟踪实用程序用“更有用”的东西来替换它们。x33g5p2x2#
$1f
是1
标签的地址。f
指定正向查找名为1
的第一个标签。"m"
是内存中的输入操作数。"=&a"
是使用eax
寄存器的输出操作数。a
指定要使用的寄存器,=
使其成为输出操作数。并且&
保证其它操作数将不共享同一寄存器。这里,
%0
将被第一个操作数(eax
寄存器)替换,%1
将被第二个操作数(address
指向的地址)替换。GCC文档中关于Inline assembly和asm约束的部分对所有这些以及更多内容进行了解释。
h7wcgrx33#
这段代码(除了由于两个错别字而不可编译之外)几乎没有什么用处。
这是它的转换结果(使用
-S
开关):所以整个函数体可以用
tez616oj4#
这是来自PintOS项目的代码片段。
这里的函数由操作系统内核使用,用于从用户地址空间**读取
address
**处的字节。这是由movzbl %1, %0
完成的,其中0%
为result
,1%
为address
。但在此之前,内核必须移动$1f
的地址(movzbl %1, %0
后面的指令地址)添加到eax
寄存器。这个动作看起来没有用,因为一些上下文信息丢失了。内核这样做是为了让页面错误中断处理程序使用它。因为address
可能是用户提供的无效的,它可能会导致页面错误。当这种情况发生时,中断处理程序会接管。将eip
设置为等于eax
(这是1f%
的内存地址),并且还将eax
设置为-1
以指示读取失败。在此之后,内核能够从处理程序返回到$1f
并继续前进。如果不保存$1f
的地址,处理程序将不知道它应该返回到哪里,并且只能一次又一次地返回到movzbl %1, %0
。