汇编实现的memcpy和memset的方法

天天山珍海味的吃,也会烦。偶尔来点花生,毛豆小酌一点,也别有一番风味。

天天java, golang, c++, 咱们今天来点汇编调剂一下,如何?

通过这篇文章,您可以了解过:

  • CPU寄存器的一些知识;
  • 函数调用的过程;
  • 汇编的一些知识;
  • glibc 中 memcpy和memset的使用;
  • 汇编中memcpy和memset是如何实现的;

闲话不多说,今天来看看汇编中如何实现memcpymemset(脑子里快回忆下你最后一次接触汇编是什么时候......)

函数是如何被调用的

栈的简单介绍

  • 栈对函数调用来说特别重要,它其实就是进程虚拟地址空间中的一部分,当然每个线程可以设置单独的调用栈(可以用户指定,也可以系统自动分配); 栈由栈基址(%ebp)和栈顶指针(%esp)组成,这两个元素组成一个栈帧,栈一般由高地址向低地址增长,将数据压栈时%esp减小,反之增大;
  • 调用一个新函数时,会产生一个新的栈帧,即将老的%ebp压栈,然后将%ebp设置成跟当前的%esp一样的值即可。函数返回后,之前压栈的数据依然出栈,这样最终之前进栈的%ebp也会出栈,即调用函数之前的栈帧被恢复了,也正是这种机制支撑了函数的多层嵌套调用;

不管是写Windows程序还是Linux程序,也不管是用什么语言来写程序,我们经常会把某个独立的功能抽出来封装成一个函数,然后在需要的地方调用即可。看似简单的用法,那它背后是如何实现的呢?一般分为四步:

函数调用规则

  • 函数一般都会有多个参数,我们根据函数调用时,
  • 参数压栈的方向(参数从左到右入栈,还是从右到左入栈);函数调用完是函数调用者负责将之前入栈的参数退栈,还是被调用函数本身来作等

这两点(其实还有一点,就是代码被编译后,生成新函数名的规则,跟我们这里介绍的关系不大)来分类函数的调用方式:

  • stdcall: 函数参数由右向左入栈, 函数调用结束后由被调用函数清除栈内数据;
  • cdecl: 函数参数由右向左入栈, 函数调用结束后由函数调用者清除栈内数据;
  • fastcall: 从左开始不大于4字节的参数放入CPU的EAX,ECX,EDX寄存器,其余参数从右向左入栈, 函数调用结束后由被调用函数清除栈内数据;

这种方式最大的不同是用寄存器来存参数,所有它fast。

glibc中的memcpy

我们先来看下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;

大家有兴趣的话可以考虑下上面的代码输出是什么?

汇编实现的memcpy

说来惭愧,汇编代码作者本人也不会写。不过我们可以参考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)

CPU的众多通用寄存器有%esi和%edi, 它们一个是源址寄存器,一个是目的寄存器,常被用来作串操作,我们的这个memcpy最终就是将%esi指向的内容拷贝到%edi中,因为这种代码在linux源码中是被标识成了.code16, 所有这里都只用到这两个寄存器的低16位:%si和%di;

代码的第一,二句保存当前的%si和%di到栈中;

这段代码实际上是fastcall调用方式,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; movslrep重复执行movsl这个操作,每执行一次%cx的内容就减一,直到为0。movsl每次从%si中拷贝4个字节到%di中。这其实就相当于一个for循环copy;

参数n不一定能被4整除,剩下的余数,我们只能一个字节一个字节的copy了。

andw $3, %cx就是对%cx取余,看还剩下多少字节没copy;

rep; movsb一个字节一个字节的copy剩下的内容;

glibc中的memset

我们先来看下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

汇编实现的memset

我们还是来看一下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, %eaximull $0x01010101,%eax

imull $0x01010101,%eax这句话就是把0x11变成0x11111111

rep; stosl,rep重复执行stosl 这个操作,每执行一次%cx的内容就减一,直到为0。stosl每次从%eax中拷贝4个字节到%di中。这其实就相当于一个for循环copy;

参数n不一定能被4整除,剩下的余数,我们只能一个字节一个字节的copy了。

andw $3, %cx就是对%cx取余,看还剩下多少字节没copy;

rep; stosl 一个字节一个字节的copy剩下的内容;

总结

以上所述是小编给大家介绍的汇编实现的memcpy和memset的方法,希望对大家有帮助!

(0)

相关推荐

  • 汇编指令:JO、JNO、JB..的使用方法

    汇编指令: JO.JNO.JB.JNB.JE.JNE.JBE.JA.JS.JNS.JP.JNP.JL.JNL.JNG.JG.JCXZ.JECXZ.JMP.JMPE 名称 功能 操作数 操作码 模数 寄存器1 寄存器2 或内存 位移量 立即数 符号 方向 芯片 型号 16位 32位 JO 溢出跳转 短 $70 无 无 无 无 10 无 无 8086 无 无 JNO 不溢出跳转 短 $71 无 无 无 无 10 无 无 8086 无 无 JB 低于跳转 短 $72 无 无 无 无 10 无 无 80

  • 汇编跳转指令使用总结

    虽然jmp指令提供了控制转移,但是它不允许进行任何复杂的判断.80x86条件跳转指令提供了这种判断.条件跳转指令是创建循环和实现其他条件执行语句.条件跳转指令检查一个或多个标志位,判断它们是否匹配某个特殊条件(就像setcc指令):如果标志匹配成功,该指令就将控制转移到目标位置:如果匹配失败,CPU忽略该条件跳转指令而继续执行下一条指令.条件跳转指令有一个限制:目标标号的位置必须在跳转指令本身附近32768字节范围内,这通常对应着8000-32000条机器指令.一般情况下不会超过这种限制. 用自

  • 汇编 JMP使用详解

    汇编 JMP 详解关键词说明 RVA: 相对虚拟地址(Relative Virtual Address),在内存中相对于PE文件装入地址的偏移位置,是一个相对地址. JMP 的 3 种类型 短跳转(Short Jmp,只能跳转到256字节的范围内),对应机器码:EB 近跳转(Near Jmp,可跳至同一段范围内的地址),对应机器码:E9 远跳转(Far Jmp,可跳至任意地址),对应机器码: EA 短跳转 和 近跳转 指令中包含的操作数都是相对于(E)IP的偏移. 远跳转指令中包含的是目标的绝对

  • 汇编语言乘指令 MUL、IMUL的具体使用

    MUL: 无符号乘 ================================================== ;影响 OF.CF 标志位 ;指令格式: ;MUL r/m  ;参数是乘数 ;如果参数是 r8/m8,   将把  AL 做乘数, 结果放在 AX ;如果参数是 r16/m16, 将把 AX 做乘数, 结果放在 EAX ;如果参数是 r32/m32, 将把 EAX 做乘数, 结果放在 EDX:EAX 当乘积的高半部分(AH.DX.EDX.RDX)中存有结果的有效数字,则CF=

  • 汇编实现的memcpy和memset的方法

    天天山珍海味的吃,也会烦.偶尔来点花生,毛豆小酌一点,也别有一番风味. 天天java, golang, c++, 咱们今天来点汇编调剂一下,如何? 通过这篇文章,您可以了解过: CPU寄存器的一些知识; 函数调用的过程; 汇编的一些知识; glibc 中 memcpy和memset的使用; 汇编中memcpy和memset是如何实现的; 闲话不多说,今天来看看汇编中如何实现memcpy和memset(脑子里快回忆下你最后一次接触汇编是什么时候......) 函数是如何被调用的 栈的简单介绍 栈对

  • C++简易版Tensor实现方法详解

    目录 基础知识铺垫 内存管理 allocate 实现Tensor需要准备shape和storage Tensor的设计方法(基础) Tensor的设计方法(更进一步) 基础知识铺垫 缺省参数 异常处理 如果有模板元编程经验更好 std::memset.std::fill.std::fill_n.std::memcpy std::memset 的内存填充单位固定为字节(char),所以不能应用与double,非char类型只适合置0. std::fill 和 std::fill_n 则可以对指定类

  • C语言双指针多方法旋转数组解题LeetCode

    目录 暴力思路 外加数组 格局抬高 环形替代 LeetCode题目如下: 首先这个中等难度我是没搞懂,后面才发现原来中等中在要求多方法上,那就来看看怎么搞定他吧. 暴力思路 首先我说一下我本人的思路,就是函数进行倒序操作,分三步: 1.整体倒序 :1234567-------7654321 2.前半部分倒序:7654321------- 5674321 3.后半部分倒序:5674321-------5671234 由于题目已经给出了我们 k 的值,我们直接暴力思路(注意是暴力思路非暴力求解),双

  • C/C++中的mem函数和strcopy函数的区别和应用

    strcpy和memcpy都是标准C库函数,它们有下面的特点. strcpy提供了字符串的复制.即strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符. memcpy提供了一般内存的复制.即memcpy对于需要复制的内容没有限制,因此用途更广. mem系列函数是面试的时候常考的知识点,我们需要熟练掌握这三个函数的原理和代码实现,要能准确无误的写出代码. memcpy.memset和memset三个函数在使用过程中,均需包含以下头文件: //在C中 #include<

  • 深入理解void以及void指针的含义

    void的含义void即"无类型",void *则为"无类型指针",可以指向任何数据类型. void指针使用规范①void指针可以指向任意类型的数据,亦即可用任意数据类型的指针对void指针赋值.例如:int *pint;void *pvoid;pvoid = pint; /* 不过不能 pint = pvoid; */如果要将pvoid赋给其他类型指针,则需要强制类型转换如:pint = (int *)pvoid; ②在ANSI C标准中,不允许对void指针进行

  • 总结了24个C++的大坑,你能躲过几个

    前段时间给部门做了个C++专题的分享,主要分享了C++语言里一些常见的坑,在这里也分享给大家. 以下是本文目录: 首先说下C++和C语言有什么区别?分享一个我在知乎上看见的回答: C++ ≈ C with classes, C with STL C:面向机器编程 C++:面向编译器编程 C++有个很重要的特性叫RAII,个人认为可以多多使用,相当方便,关于RAII巧妙使用可以看我这两篇文章<RAII妙用之ScopeExit><RAII妙用之计算函数耗时>. 言归正传,下面我一个一个

  • C语言动态数组详解

    目录 内存分配函数malloc calloc realloc free 内存操作函数 memset memcpy memmove 二维动态数组的建立和释放 总结 内存分配函数malloc calloc realloc free 堆内存分配函数 说明 void * malloc(int n) 形参n为要求分配的字节数.需要注意的是,malloc函数分配得到的内存空间是未初始化的.必须使用memset函数来初始化. calloc(10, sizeof(char)); 两个参数:单元数,单元的size

  • linux内核copy_{to, from}_user()的思考

    目录 一.什么是copy_{to,from}_user() 1.copy_{to,from}_user()对比memcpy() 2.函数定义 二.CONFIG_ARM64_SW_TTBR0_PAN原理 三.测试 四.总结 一.什么是copy_{to,from}_user() 它是kernel space和user space沟通的桥梁.所有的数据交互都应该使用类似这种接口.但是他的作用究竟是什么呢?我们对下提出疑问: 为什么需要copy_{to,from}_user(),它究竟在背后为我们做了什

  • 如何写好C main函数的几个注意事项

    学习如何构造一个 C 文件并编写一个 C main 函数来成功地处理命令行参数. 我知道,现在孩子们用 Python 和 JavaScript 编写他们的疯狂"应用程序".但是不要这么快就否定 C 语言 -- 它能够提供很多东西,并且简洁.如果你需要速度,用 C 语言编写可能就是你的答案.如果你正在寻找稳定的职业或者想学习如何捕获空指针解引用,C 语言也可能是你的答案!在本文中,我将解释如何构造一个 C 文件并编写一个 C main 函数来成功地处理命令行参数. 我:一个顽固的 Uni

  • C/C++中memset,memcpy的使用及fill对数组的操作

    对数组的整体赋值,以及两个数组间的复制容易出错,这里使用string头文件中的memset和memcpy进行 不必遍历数组,速度快. 之前没有头文件,显示decla 头文件: 代码: /* Project: 数组的整体赋值与复制 Date: 2018/07/31 Author: Frank Yu memset(数组名,0或-1,字节) memcpy(数组名,数组名,字节) */ #include<iostream> #include<cstring> //memset需要头文件 #

随机推荐