天天山珍海味的吃,也会烦。偶尔来点花生,毛豆小酌一点,也别有一番风味。
天天java, golang, c++, 咱们今天来点汇编调剂一下,如何?
通过这篇文章,您可以了解过:
闲话不多说,今天来看看汇编中如何实现memcpy
和memset
(脑子里快回忆下你最后一次接触汇编是什么时候......)
不管是写Windows程序还是Linux程序,也不管是用什么语言来写程序,我们经常会把某个独立的功能抽出来封装成一个函数,然后在需要的地方调用即可。看似简单的用法,那它背后是如何实现的呢?一般分为四步:
栈
来传递参数,先将所有参数都压栈处理;函数一般都会有多个参数,我们根据函数调用时,
这两点(其实还有一点,就是代码被编译后,生成新函数名的规则,跟我们这里介绍的关系不大)来分类函数的调用方式:
我们先来看下glibc中的memcpy , 原型如下:
void *memcpy(void *dest, const void *src, size_t n);
从src拷贝连续的n个字节数据到dest中, 不会有任何的内存越界检查。
char dest[5] = {0};
char test[5] = {0,'b'};
char src[10] = {'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'};
::memcpy(dest, src, 6);
std::cout << src << std::endl;
std::cout << dest << std::endl;
std::cout << test << std::endl;
大家有兴趣的话可以考虑下上面的代码输出是什么?
说来惭愧,汇编代码作者本人也不会写。不过我们可以参考linux源码里面的实现,这相对还是比较权威的吧。
它的实现位于 arch/x86/boot/copy.S
, 文件开头有这么一行注释Copyright (C) 1991, 1992 Linus Torvalds
, 看起来应该是大神亲手写下的。我们来看一看
GLOBAL(memcpy)
pushw %si
pushw %di
movw %ax, %di
movw %dx, %si
pushw %cx
shrw $2, %cx
rep; movsl
popw %cx
andw $3, %cx
rep; movsb
popw %di
popw %si
retl
ENDPROC(memcpy)
.code16
, 所有这里都只用到这两个寄存器的低16位:%si和%di;void *memcpy(void *dest, const void *src, size_t n);
其中 dest 被放在了%ax寄存器,src被放在了%dx, n被放在了%cx;
movw %ax, %di
, 将dest放入%di中,movw %dx, %s
,将stc放入%si中;shrw $2, %cx
,看看参数n里面有几个4, 我们就需要循环拷贝几次,循环的次数存在%cx中,因为后面还要用到这个%cx, 所以计算之前先将其压栈保存 pushw %cx
;rep; movsl
, rep
重复执行movsl
这个操作,每执行一次%cx的内容就减一,直到为0。movsl每次从%si中拷贝4个字节到%di中。这其实就相当于一个for循环copy;andw $3, %cx
就是对%cx取余,看还剩下多少字节没copy;
rep; movsb
一个字节一个字节的copy剩下的内容;我们先来看下glibc中的memset, 原型如下:
void *memset(void *s, int c, size_t n);
这个函数的作用是用第二个参数的最低位一个字节来填充s地址开始的n个字节,尽管第二个参数是个int, 但是填充时只会用到它最低位的一个字节。
你可以试一下下面代码的输出:
int c = 0x44332211;
int s = 0;
::memset((void*)&s, c, sizeof(s));
std::cout << std::setbase(16) << s << std::endl; // 11111111
我们还是来看一下arch/x86/boot/copy.S
中的实现:
GLOBAL(memset)
pushw %di
movw %ax, %di
movzbl %dl, %eax
imull $0x01010101,%eax
pushw %cx
shrw $2, %cx
rep; stosl
popw %cx
andw $3, %cx
rep; stosb
popw %di
retl
ENDPROC(memset)
memcpy
,这里不需要%si源址寄存器,只需要目的寄存器,所以我们先将其压栈保存 pushw %di
;void *memset(void *s, int c, size_t n)
可知,参数s被放在了%ax寄存器;参数n被放在了%cx寄存器;
参数c被放在了%dl寄存器,这里只用到了%edx寄存器的最低一个字节,所以对于c这个参数不管你是几个字节,其实多只有最低一个字节被用到;
memcpy
一样,一次一个字节的操作太慢了,一次四个字节吧,假设参数c的最低一个字节是0x11, 那么一次set四个字节的话,就是0x11111111:
movzbl %dl, %eax imull $0x01010101,%eax
imull $0x01010101,%eax
这句话就是把0x11
变成0x11111111
rep; stosl,
rep重复执行
stosl 这个操作,每执行一次%cx的内容就减一,直到为0。stosl每次从%eax中拷贝4个字节到%di中。这其实就相当于一个for循环copy;andw $3, %cx
就是对%cx取余,看还剩下多少字节没copy;
好了,到这里这次的内容就结束了,有疏漏之处,欢迎指正。