今天有同事问我之前写的那篇 iOS 常见 Crash 及解决方案 里面粘贴的 GLibC 关于 memcpy 的代码怎么理解,然后我囧了一下,当时就是随手一 copy,其实没理解透,于是花了点时间看了一下,学了不少东西,写篇博客记录一下。这里真得感谢一下 @raincai 同学的提醒。之前我粘贴的代码如下:
#define BYTE_COPY_FWD(dst_bp, src_bp, nbytes) \
do { \
int __d0; \
asm volatile(/* Clear the direction flag, so copying goes forward. */ \
"cld\n" \
/* Copy bytes. */ \
"rep\n" \
"movsb" : \
"=D" (dst_bp), "=S" (src_bp), "=c" (__d0) : \
"0" (dst_bp), "1" (src_bp), "2" (nbytes) : \
"memory"); \
} while (0)
其实上面这段代码有点问题,整理一下应该是这样:
#define BYTE_COPY_FWD(dst_bp, src_bp, nbytes) \
do {
__asm__ __volatile__ (/* Clear the direction flag, so copying goes forward. */ \
"cld\n" \
/* Copy bytes. */ \
"rep\n" \
"movsb"
:"=D" (dst_bp), "=S" (src_bp), "=c" (__d0) \
:"0" (dst_bp), "1" (src_bp), "2" (nbytes) \
:"memory");
} while (0)
我们一步步来解,看到已经理解的直接跳过就是了。
一、关键字
- do while 0
linux内核代码很多宏都要加上这个,主要是为了是为了防止被调用的时候,复杂语句有些没被执行到。
举个栗子:
#define SOMETHING()\ fun1();\ fun2();
这个宏是为了能执行到 fun1 和 fun2,但是如果你调用这个宏的时候,加上了条件判断:
if (condition == true) SOMETHING();
那就悲剧了,预编译的时候,宏定义被代码替换掉,那就是
if (condition == true) fun1(); fun2();
fun2()就掉到判断的外面去了。所以加上这个是为了保险。
- asm
这个其实就是用于在 C 语言内嵌汇编的关键字 asm, 有下划线的是个宏,看源码是这样定义的:
#ifndef __GNUC__ #define __asm__ asm #endif
-
volatile
跟 asm 类似,带下划线就是个宏,其实就是 volatile 关键字:#define __volatile__ volatile
带上这个关键字就是告诉 GCC 不要做优化,要完全保留我写的指令,不要做任何修改。所以这个关键字是可选的。
所以总的来说,在 C 语言里面,内嵌汇编的写法就是
__asm__ ("汇编代码段") 或者 __asm__ __volatile__ (指定操作 + "汇编代码段")
二、汇编代码
- cld
复位方向表标记位 DF,即 DF = 0。DF为 0 则源寄存器地址 ESI/EDI (源寄存器/目标寄存器) 递增,1 则递减。
- rep
表示重复,repeat,当 ECX (计数器) > 0 的时候就一直 rep。
- movsb
就是搬移字串,汇编搬移字串有 movsb 和 movsw 两种,movsb 就是 moving string byte,就是一次搬一个字节,mvsw就是搬移字了
- 还有几个寄存器关键字
EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP等都是X86汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。如果用C语言来解释,可以把这些寄存器当作变量看待。
EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
EBX 是"基地址"(base)寄存器, 在内存寻址时存放基地址。
ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
EDX 则总是被用来放整数除法产生的余数。
ESI/EDI 分别叫做"源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.
EBP 是"基址指针"(BASE POINTER), 它最经常被用作高级语言函数调用的"框架指针"(frame pointer). - 所以上面 =D 代表设置 EDI 目标索引寄存器,=S 是 ESI 源索引寄存器,=c 是 ECX 计数器
OK,接下来是那些冒号,插入C代码中的一个汇编语言代码片断可以分成四部分,以“:”号加以分隔,其一般形式为:
指令部:输出部:输入部:损坏部
- 指令部就是上面几个指令啦无需多言,我们先看输出部:
=D 这样的语句是对输出部的约束条件:
常用约束条件一览
m, v, o —— 表示内存单元; r —— 表示任何寄存器; q —— 表示寄存器eax、ebx、ecx、edx之一; i, h —— 表示直接操作数; E, F —— 表示浮点数; g —— 表示”任意“; a, b, c, d —— 分表表示要求使用寄存器eax、ebx、ecx和edx; S, D —— 分别表示要求使用寄存器esi和edi; I —— 表示常数(0到31)。
所以 "=D" (dst_bp), "=S" (src_bp), "=c" (__d0) 就是把 dst_bp 放进 EDI 寄存器, src_bp 放进 ESI 寄存器, __d0 放进 ECX 寄存器。
- 再来看输入部
:"0" (dst_bp), "1" (src_bp), "2" (nbytes) 这里的 0, 1, 2 不属于上面约束条件的字母,而是数字,数字代表跟输出部的第 0/1/2 个约束条件是同一个寄存器,那就很好理解了,就是说 EDI 寄存器里面将会输入 dst_bp, ESI 会输入 src_bp,最后的 ECX 会输入 nbytes 这个变量。
- 最后看损坏部
这里以“memory”为约束条件,表示操作完成后内存中的内容已有改变,如果原来某个寄存器(也许在本次操作中并未用到)的内容来自内存,则现在可能已经不一致。
-
总的来说就是使用movsb指令来按字节搬运字符串,先设置了 EDI, ESI, ECX 几个寄存器的值, 其中EDI寄存器存放拷贝的目的地址,ESI寄存器存放拷贝的源地址,ECX为需要拷贝的字节数。所以最后汇编执行完之后,EDI中的值会保存到dst_bp中,ESI中的值会保存到src_bp中。
其他版本
这个函数有几个版本的,上面是汇编版本,下面这个是 C 版本,这个就很好理解了:
do \
{ \
size_t __nbytes = (nbytes); \
while (__nbytes > 0) \
{ \
byte __x = ((byte *) src_bp)[0]; \
src_bp += 1; \
__nbytes -= 1; \
((byte *) dst_bp)[0] = __x; \
dst_bp += 1; \
} \
} while (0)