asm goto与JUMP_LABEL

版权声明:本文为优快云博主「dog250」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.youkuaiyun.com/dog250/article/details/6123517


越来越多的工作现如今都交给了编译器,甚至连动态代码修改的数据组织这种事都交给了编译器。gcc提供了一个特性用于嵌入式汇编,那就是asm goto,其实这个特性没有什么神秘之处,就是在嵌入式汇编中go to到c代码的label,其最简单的用法如下(来自gcc的文档):

asm goto其实就是在outputs,inputs,registers-modified之外提供了嵌入式汇编的第四个“:”,后面可以跟一系列的c语言的label,然后你可以在嵌入式汇编中go to到这些label中一个。然而使用asm goto可以巧妙地将“一个大家都能想到的点子”规范化,就是说你只需要调用一个统一的接口--一个宏,编译器就将你想实现的东西给实现了,要不然代码写起来会很麻烦,这点上,编译器不嫌麻烦。这一个大家都能想出的点子的由来还得从内核的效率说起。

以下的代码来自lwn的《Jump label》:

即使有了unlikey优化,既然有if判断,cpu的分支预测就有可能失败,再者do_trace在代码上离if这么近,即使编译器再聪明,二进制代码的do_trace也不会离前面的代码太远的,这样由于局部性原理和cpu的预取机制,do_trace的代码很有可能就被预取入了cpu的cache,就算我们从来不打算trace代码也是如此。

     我们需要的是如果不开启trace,那么do_trace永远不被欲取或者被预测,唯一的办法就是去掉if判断,永远不调用goto语句,像下面这样:

在运行时修改载入内存的二进制代码就是我们大家都能想到的点子,就是说在运行的时候当我们知道trace_foo_enabled在某一时刻被设置为0的时候,我们动态的将二进制代码修改掉,将if代码段去掉,这样一个分支预测就不存在了,而且trace_foo_enabled这一个变量也不需要再被访问了(该变量在内存中,访问它肯定会涉及load/flush cache的动作,为了一个很可能没有用的变量操作cache很不值)。提前要说的是,我们可以使用这种方式去掉所有的分支预测,然而这并不可取,因为程序是动态运行的,很多用于判断的变量值都是根据程序的执行瞬息万变,正是这种根据判断结果采取不同动作的机制给与了程序灵活性,如果每当我们确定一个值时就修改二进制代码取消分支预测的话,其本身的开销将会远远大于分支预测的开销,更重要的是,紧接着那个值又变化了,我们不得不再次修改二进制代码,这期间要访问那个变量好几次。所以,只有在我们确定不经常变化的变量的判断上才能用这种方式取消分支预测,而像trace与否的判断正好符合我们的需求。

     gcc编译器提供了asm goto的机制来满足我们的需求,使得我们可以在asm goto的基础上构建出一个叫做jump label的东西。下面的代码段说明了jump label的用法和原理:

标号0仅仅执行一个nop,不涉及cache,后面的pushp保存现有的p,很多情况下当前的p就是text,然后定义一个“表”,表中有两个元素:0b和trace#NUM,其实就是两个标号,在asm goto机制中,标号还可以更多,它们在嵌入式汇编的最后一个“:”后面依次排布。这些标号就是供选择的标号,执行流将跳入其中的一个标号处,具体跳到哪一个就看当前的二进制代码被修改成了“跳到哪一个”,因此asm goto为我们做的仅仅是提供一个地方(一个“:”)供我们将label传入,保存了一系列的表还是需要我们的c代码逻辑--jump label实现,这些表(其实就是一系列的三元组)方便我们根据这些表来修改运行中的二进制代码,最终修改二进制代码还是要由我们自己写代码完成的。

     有了这个asm goto以及我们jump label代码的支持,内核对于是否trace这种小事就再也不用愁了(使用中的kernel一般是不用trace的,只有在出了问题以后或者调试内核时才使用trace,因此在主代码中加入“是否trace”的判断实在是一种沉重的负担),如果对于某一个函数不需要trace,内核只需要执行一个操作将asm goto附近的代码改掉即可,比如改称下面这样:

如果需要trace,那么就改成:

这一切在kernel中的用法如下:

第一行的“1”是一个标号,该标号后的代码执行的内容就是nop-第二行,第三行重新开始了一个p,这样的意义很大,下面的三元组:[instruction address] [jump target] [tracepoint key]的二进制代码就不会紧接着标号1(nop)了,这个三元组就是jump label机制的核心,指示了所有可能跳转到的标号,这里的技巧在于标号1,标号1也作为一个合法的可能跳转到的标号存在,和标号label是并列的,由于pushp和popp的存在,上面的代码汇编结果看起来是下面这样:

如果启用了trace,那么只需要将标号1修改成标号label就可以了:

内核之所以能够找到需要修改代码的地址,就是借助于上面说的那个三元组(instruction address,jump target,tracepoint key),其中instruction address就是这个地址,在linux的JUMP  LABEL机制中,它固定为标号1,也就是nop的标号,如果不启用trace,那么直接执行nop,如果启用了trace,那么将nop修改为jmp label即可,如果后来又禁用了trace,只需将它再次修改成三元组中的标号1即可,这一切过程中,三元组本身是不会改变的。注意,三元组中的tracepoint key在jump label机制中并没有什么实质的意义,它仅仅是为了组织kernel中“是否trace”变量用的,所有的“是否trace”变量组织成一个链表,链表的每一个节点下面挂着另一个子链表,该子链表中元素是所有使用这个“是否trace”变量的代码环境,包括代码的地址,标号的地址等。

     下面看一下kernel对于JUMP_LABEL的实现框架。首先看一下三元组的数据结构:

其次一个比较重要的数据结构是一个key节点,表示一个“是否trace”的变量:

启用一个trace意味着需要将一个key(类似于trace_foo_enabled)设置为1,然后修改所有判断该key的代码附近的二进制代码:

以上就是使用asm goto实现的jump label,在2.6.37内核中被引入。

附:.p以及.previous

在汇编语言中使用.p和.previous指令可以将它们之间的代码编译到不同的p中,也就是不紧接着.p上面的代码。linux kernel中的异常处理就是用这两个伪指令实现的,定义了一个叫做fix的p和一个叫做ex_table的p,可能出现exception的代码用一个标号表示,ex_table中保存了一些二元组(出现异常代码的标号,异常处理程序的标号),异常处理程序在fix这个p中,这样虽然代码看起来是下面这样:

然而编译器会将fix和ex_table放到离text很远的地方的,这样cpu预取时就不会将fix或者ex_table的代码预取到执行cache了,只有在发生异常的时候才会使用fix和ex_table,而发生异常毕竟是一种罕见现象,这就是一种优化。

更多精彩,尽在"Linux阅码场",扫描下方二维码关注

别忘了分享、点赞或者在看哦~

<think>嗯,用户提供的代码片段看起来是关于Android的Bionic C库中的一些函数声明和内联函数的使用。让我先仔细分析一下这些代码的结构和各个部分的作用。 首先,用户提到了`extern int __property_get_real(const char *, char *, const char *) __asm__(__USER_LABEL_PREFIX__ "property_get");`。这里的关键点在于`extern`关键字,这表明这个函数是在其他地方定义的,可能是在另一个源文件或者库中。`__asm__`用来指定函数的汇编标签,这里将`__property_get_real`的汇编符号设置为`property_get`,可能涉及到函数重命名或者别名。`__USER_LABEL_PREFIX__`通常是一个宏,用来处理不同平台下的符号前缀,比如在有些平台上可能会添加下划线。 接下来是`__errordecl(__property_get_too_small_error, __property_get_err_str);`。`__errordecl`应该是用来声明一个错误类型的宏,第一个参数是错误名称,第二个参数是错误信息字符串。这可能用于在编译时生成特定的错误信息,当某些条件不满足时触发这个错误。 然后,`__BIONIC_FORTIFY_INLINE`这个宏应该Bionic库的强化(fortify)功能相关,可能用于启用内联函数的安全检查,比如缓冲区溢出检测。Fortify是GCC的一种安全特性,用于在编译时增加对某些函数调用的检查,防止常见的安全漏洞。 现在需要将这些部分联系起来,理解整个代码的作用。看起来这些代码是在重新声明或包装`property_get`函数,并添加了错误检查和强化措施。`__property_get_real`可能是实际的实现,而通过`__asm__`重命名后,外部调用`property_get`时会使用这个函数。`__errordecl`可能在编译时检查缓冲区大小是否足够,如果不够,就会抛出错误。而`__BIONIC_FORTIFY_INLINE`可能用于生成内联的强化版本函数,增加运行时检查。 可能用户是在查看Android系统源码时遇到了这些宏和声明,想要了解它们的作用和背后的机制。需要进一步解释这些宏如何增强安全性,以及它们是如何在Bionic库中协同工作的。同时,用户可能对`property_get`函数本身的作用感兴趣,这个函数在Android中用于读取系统属性,比如`ro.build.version`等。 需要确保解释清晰,分点说明每个部分的功能,并结合实际例子帮助用户理解。还要注意用户可能不熟悉Bionic库或Fortify特性,需要适当补充背景知识。最后,总结整个代码段的目的和实现的安全措施,让用户有一个整体的认识。</think>这个代码片段涉及 Android Bionic C 库的底层实现机制,我来为您分层解析: 1. **函数重定向机制** ```c extern int __property_get_real(const char *, char *, const char *) __asm__(__USER_LABEL_PREFIX__ "property_get"); ``` - `extern` 声明外部实现的真实函数 - `__asm__` 指定汇编标签,将 `__property_get_real` 映射到 `property_get` 符号 - `__USER_LABEL_PREFIX__` 处理不同平台的符号前缀(如 macOS 的 `_` 前缀) 2. **编译时错误检测** ```c __errordecl(__property_get_too_small_error, __property_get_err_str); ``` - 声明名为 `__property_get_too_small_error` 的编译错误 - 当检测到缓冲区过小时,输出 `__property_get_err_str` 定义的错误信息 3. **安全强化机制** ```c __BIONIC_FORTIFY_INLINE ``` - 启用 Bionic 的 `_FORTIFY_SOURCE` 强化功能 - 在编译时插入缓冲区溢出检查等安全防护 - 典型检查包括: - 目标缓冲区大小验证 - 格式字符串漏洞防护 - 无效指针检测 4. **实际应用场景** ```c // 使用者看到的接口 int property_get(const char* key, char* value); // 实际调用过程 char buf[32]; property_get("ro.build.version", buf, ""); // 触发缓冲区检查 ``` 当开发者调用 `property_get` 时: - 编译时检查目标缓冲区大小 - 如果 `buf` 小于属性值所需空间,触发 `__property_get_too_small_error` - 错误信息示例:`error: buffer size 32 is too small for property value` 5. **设计目的** - 保持源码级兼容性 - 在二进制层面增强安全性 - 通过静态检查预防常见内存错误 - 对性能影响极小(大部分检查在编译时完成) 这个实现模式广泛存在于 Android 系统调用(如 `open`、`read` 等),体现了系统级编程中安全性兼容性平衡的典型实践。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值