首页
社区
课程
招聘
SIMD指令学习
发表于: 2025-1-14 18:32 5039

SIMD指令学习

2025-1-14 18:32
5039

SIMD指令学习

一、简介:

1、什么是SIMD?

​ SIMD(Single Instruction, Multiple Data)是一种并行计算技术,通俗来说就是单指令多数据流,它通过向量寄存器存储多个数据元素,并使用单条指令同时对这些数据元素进行处理,从而提高了计算效率。SIMD已被广泛应用于需要大量数据并行计算的领域,包括图像处理、视频编码、信号处理、科学计算等,尤其是在需要处理大量数据的场景下,能够显著提高性能。

​ 而该项技术发展至今,已然是较为成熟了,发展过程中产生了很多套指令集,简单介绍一下,相信不少师傅在做逆向题的时候已经遇到不少了。

2、指令集的发展

2.1、MMX (Multimedia Extensions)

​ MMX是Intel在1996年推出的SIMD指令集,它在x86处理器上提供了并行处理整数数据的能力。MMX的核心特性是它通过扩展x86架构的寄存器和指令,支持并行处理多个整数数据。MMX使用的是64位寄存器,每个寄存器可以存储多个数据元素(比如8个8位整数,4个16位整数等)。

  • 支持数据类型:整数
  • 寄存器:MM0-MM7 (每个寄存器64位)
  • 常用指令:PADDQPMULUDQ

​ MMX虽然具有并行处理整数的能力,但在浮点运算方面的支持较弱,后来被SSE(Streaming SIMD Extensions)系列指令集所取代。

2.2、SSE/SSE2/SSE3/SSE4(Streaming SIMD Extensions)

​ SSE是Intel在1999年推出的指令集,旨在增强浮点运算的性能。SSE扩展了MMX指令集,并支持浮点和整数运算。SSE支持单精度浮点数运算以及整数运算等指令,并引入了8个独立的128位寄存器,称为XMM0-XMM7。后续发布的SSE2指令集则一方面添加了对双精度浮点数的支持,另一方面也增添了整数处理指令,这些新的整数处理指令能够覆盖MMX指令的功能,从而让旧的MMX指令显得多余。2003年,AMD推出AMD64架构时,又新增了8个XMM寄存器,它们被称为XMM8-XMM15。当CPU处于32位模式时,可用的XMM寄存器为XMM0-XMM7,而当CPU处于64位模式时,可用的XMM寄存器为XMM0-XMM15。此后推出的SSE3/SSE4又添加了更多了SIMD指令。

  • 支持数据类型:32位和64位浮点数、整数
  • 寄存器:XMM0-XMM15(每个寄存器128位)
  • 常用指令:ADDPS(加法)、MULPS(乘法)、SUBPS(减法)

​ SSE可以同时对4个32位浮点数进行并行计算,相比于MMX,SSE的浮点运算能力更强。SSE2极大地增强了对双精度浮点数和整数的支持,成为了现代x86处理器中最常用的SIMD指令集之一。SSE3在SSE2的基础上进一步扩展,增加了一些新的指令,主要针对双精度浮点运算、复数计算等。SSE4进一步增强了SIMD的功能,增加了SSE4.1和SSE4.2两个子集,提升了多媒体、加密、解密以及字符串处理的性能。

2.3、AVX/AVX2/AVX-512(Advanced Vector Extensions)

​ AVX是Intel在2011年推出的扩展,支持256位的寄存器(YMM0-YMM15),并支持更高精度的浮点运算,且允许每个指令处理8个32位浮点数,因此相比于SSE,AVX具有更高的吞吐量。AVX2是AVX的进一步扩展,支持256位的整数和浮点数运算,并且新增了对整数的高级操作,提升了整数运算的并行度,其提升了大数据处理和加密算法的性能,是现代x86处理器中非常重要的指令集。AVX-512是AVX的进一步扩展,支持512位的寄存器,并提供了更多的高级指令,用于大规模数据并行处理和科学计算,其提供了极高的吞吐量,尤其在大规模数据处理和高性能计算中表现优秀,但它的功耗较大,实际应用中并不适用于所有场合。

3、总结

​ 综上所述,我们已经对于SIMD指令这一项技术的发展有了大致的了解,同时对于他们的用法和用途也有了初步认识。x86平台的SIMD指令集从MMX到AVX-512逐步发展,逐渐增加了对浮点数、整数、256位和512位寄存器的支持,性能不断提升。在现代应用中,AVX2和AVX-512的使用越来越普遍,而SSE和MMX则多用于兼容旧系统。

​ 每一代的指令集都兼容上一代,也就是说新一代的指令集也支持使用上一代的指令和寄存器(但硬件实现可能有区别)。此外,AVX对之前的部分指令进行了重构,所以不同代际之间相同功能的函数可能具有不同的接口。不同代际的指令尽量不要混用,因为每次状态切换会有性能消耗,从而拖慢程序的运行速度。代际之间对寄存器及其位宽的更新情况如下:

指令集 寄存器 浮点位宽 整型位宽
MMX MM0~MM7 64
SSE XMM0~XMM7 128
SSE2 XMM0~XMM15 128 128
AVX YMM0~YMM15 256 128
AVX2 YMM0~YMM15 256 256

二、如何使用

​ 在C++程序中使用SIMD指令有两种方案,一种是使用内联汇编,另一种是使用intrinsic函数。以简单的数组相乘为例,代码的常规写法、内联汇编写法以及intrinsic函数写法分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
float a[4] = { 1.0, 2.0, 3.0, 4.0 };
float b[4] = { 5.0, 6.0, 7.0, 8.0 };
float c[4];
 
// 常规写法,用循环实现数组相乘
for (int i = 0; i < 4; ++i)
{
    c[i] = a[i] * b[i];
}
 
// 使用SIMD指令的内联汇编
__asm
{
    movups xmm0, [a];  // 将a所指内存的128位数据放入xmm0寄存器
    movups xmm1, [b];  // 将b所指内存的128位数据放入xmm1寄存器
    mulps xmm0, xmm1;  // 计算xmm0 * xmm1(4个32位单精度浮点数对位相乘),结果放入xmm0
    movups[c], xmm0;   // 将xmm0的数据放入c所指内存
}
 
// 使用intrinsic函数
__m128 va = _mm_loadu_ps(a);
__m128 vb = _mm_loadu_ps(b);
__m128 vc = _mm_mul_ps(va, vb);
_mm_storeu_ps(c, vc);
__m128i vd = _mm_set_epi64x(long long int e1, long long int e0);

三、例题演示

​ 这里选择一道24年国成杯线下的Re题(HappyENC)来做讲解,载入ida之后映入眼帘的就是一堆SIMD指令,看着吓人,其实仔细分析之后,逻辑也是比较清晰的

QQ_1736845475784

​ 先看最后的密文比较部分可以知道,总共需要传入的数据总长为96

QQ_1736845595546

QQ_1736845604049

​ 接着思路往前逆回去,发现这边有两个分支,根据dword_7FF65AF05058的值来走判断,下面的vpmullq指令源于ARM架构NEON SIMD 指令集,针对 64位整数乘法 运算,那么逻辑也就清晰了,这部分应该是为了做兼容而设置的

QQ_1736845809248

​ 继续往回看,首先输入内容会被排成6组,每一组是16个字节,总共也就是96个字节,然后有一个对于启动参数的判断,如果在程序启动时直接在后面跟参数的话,si128的值就会受影响,其取值有两种可能(这里使用flag头输入之后动调测试了一下,没有参数传入的分支才是对的),接着就是一些使用SIMD指令来做的异或还有相加之类的操作,后续写脚本到这儿直接按着思路撸代码就好了

QQ_1736846378233

​ ok,现在前半部分的思路都大概清晰了,但是对于做完各种异或和相加之后的相乘操作还是有点朦胧,那么这里直接动调跑起来看看到底是如何做那一步的,0A142AF62CA5B79D7给到寄存器r8之后,r8与rax相乘继续存入原来的位置(rax从rcx取的,然后rcx往后加8个字节的位置赋给rcx,最后rax存入的此时的rcx的位置即是它原来的位置)

QQ_1736847330142

​ 那么此处的逻辑即是输入的内容经过前面的异或相加变换之后,每八个字节为一组,与0A142AF62CA5B79D7相乘,得到的结果的低8个字节覆盖原来的内容存入,下图是输入测试内容flag{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}经过一系列异或相加和上面的相乘操作之后存入的内容,不难看出前五个字节计算出来的结果是与密文相对应的,那么就说明笔者整体分析的逻辑是对劲的

QQ_1736847979707

​ 对于乘法这里的思路是采取求模逆的方法来做,两个大数相乘,直接除是除不回去的,于是直接从网上找个板子求一下0x0A142AF62CA5B79D7的逆元即可继续编写代码往回逆

​ 先使用下列脚本规范一下提取出来的密文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <iostream>
#include <xmmintrin.h>
int main()
{
    unsigned long unk_140003280[96] = {
    0x00000000000000BF, 0x000000000000002E, 0x00000000000000DD, 0x0000000000000028,
    0x0000000000000097, 0x0000000000000084, 0x00000000000000B5, 0x0000000000000040,
    0x000000000000005F, 0x00000000000000A9, 0x000000000000005D, 0x0000000000000018,
    0x00000000000000A8, 0x000000000000000C, 0x0000000000000098, 0x0000000000000009,
    0x00000000000000FA, 0x000000000000006D, 0x00000000000000B1, 0x0000000000000032,
    0x00000000000000B7, 0x00000000000000A6, 0x000000000000001A, 0x0000000000000079,
    0x00000000000000CF, 0x00000000000000CA, 0x00000000000000AD, 0x000000000000002E,
    0x0000000000000059, 0x00000000000000B2, 0x0000000000000056, 0x00000000000000D4,
    0x00000000000000AA, 0x000000000000002B, 0x000000000000000D, 0x0000000000000042,
    0x0000000000000090, 0x0000000000000083, 0x00000000000000C0, 0x000000000000000F,
    0x0000000000000016, 0x0000000000000030, 0x0000000000000027, 0x0000000000000081,
    0x00000000000000CD, 0x000000000000009C, 0x0000000000000038, 0x00000000000000C0,
    0x00000000000000F1, 0x000000000000004D, 0x00000000000000A5, 0x000000000000007F,
    0x0000000000000062, 0x0000000000000076, 0x00000000000000AE, 0x00000000000000DD,
    0x000000000000009B, 0x0000000000000032, 0x00000000000000D8, 0x000000000000003C,
    0x00000000000000CA, 0x000000000000000D, 0x00000000000000A2, 0x00000000000000D4,
    0x00000000000000D5, 0x00000000000000DD, 0x000000000000002C, 0x000000000000006D,
    0x00000000000000FB, 0x000000000000000E, 0x000000000000005D, 0x00000000000000CD,
    0x00000000000000F9, 0x0000000000000088, 0x000000000000006B, 0x00000000000000A1,
    0x0000000000000002, 0x00000000000000AD, 0x0000000000000079, 0x000000000000001E,
    0x000000000000009E, 0x00000000000000AF, 0x0000000000000006, 0x000000000000001D,
    0x0000000000000052, 0x000000000000001E, 0x0000000000000031, 0x00000000000000D1,
    0x0000000000000002, 0x0000000000000053, 0x0000000000000096, 0x000000000000006C,
    0x0000000000000004, 0x000000000000004C, 0x0000000000000030, 0x000000000000002E
};
    unsigned char temp[16]={0};
     
    for(int i=0;i<96;)
    {
        for(int j=0;j<8;j++)
        {
            temp[j]=unk_140003280[i];
            i++;
        }
        printf("0x");
        for(int j=7;j>=0;j--)
        {
            printf("%02x",temp[j]);
         }
        printf(",\n");
    }
    return 0;
 }

然后编写脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <stdio.h>
#include <xmmintrin.h>
 
// 辗转相除法(扩展欧几里得算法)求模反元素
unsigned long long mod_inverse(unsigned long long a,unsigned long long mod) {
    unsigned long long m0 = mod, t, q;
    unsigned long long x0 = 0, x1 = 1;
 
    if (mod == 1) return 0;  // 如果模是 1,则没有反元素
 
    while (a > 1) {
        // 商
        q = a / mod;
        t = mod;
        mod = a % mod;
        a = t;
        t = x0;
        x0 = x1 - q * x0;
        x1 = t;
    }
 
    // 如果 x1 小于 0,则调整为正数
    if (x1 < 0) x1 += m0;
 
    return x1;
}
int main() {
 unsigned long long table[12] = {
    0x40b5849728dd2ebf,
    0x09980ca8185da95f,
    0x791aa6b732b16dfa,
    0xd456b2592eadcacf,
    0x0fc08390420d2baa,
    0xc0389ccd81273016,
    0xddae76627fa54df1,
    0xd4a20dca3cd8329b,
    0xcd5d0efb6d2cddd5,
    0x1e79ad02a16b88f9,
    0xd1311e521d06af9e,
    0x2e304c046c965302
 };
 const unsigned long long MOD = 1LL << 64 - 1; //MOD即规定了找逆元的运算范围,1LL << 64 - 1即表示了64位无符号整数的最大值,于这个范围之内取寻找逆元
 
 unsigned long long inv = mod_inverse(0xA142AF62CA5B79D7, MOD);
 unsigned long long original[12];
 for (int i = 0; i < 12; i++) {
 original[i] = (table[i] * inv);
 }
 __m128i data[7] = { 0 };
 for (int i = 0; i < 6; i++) {
 data[i] = _mm_loadu_si128((__m128i*)(original + 2 * i));
 }
 __m128i addNumber = _mm_set_epi64x(0xe2b7a692404d29f1, 0x22);
 data[5] = _mm_sub_epi64(data[5], addNumber);
 data[4] = _mm_sub_epi64(data[4], addNumber);
 data[2] = _mm_sub_epi64(data[2], addNumber);
 data[1] = _mm_sub_epi64(data[1], addNumber);
  
 
 __m128i x0rData = _mm_set_epi64x(0xa2e8cb53e715cedf, 0xbb6585dd9353093f);
 data[5] = _mm_xor_si128(data[4], data[5]);
 data[4] = _mm_xor_si128(data[3], data[4]);
 data[3] = _mm_xor_si128(data[2], data[3]);
 data[2] = _mm_xor_si128(data[2], data[1]);
 data[1] = _mm_xor_si128(data[0], data[1]);
 data[0] = _mm_xor_si128(data[0], x0rData);
  
 printf("%s", (char*)data);
 return 0;
}
//flag{33dfe56d-dc-c9478cea6641-6c0f-2a89a1b24b3a33-07fbc-4ccdff9-fe0a-9a5c30cff1fa-0aca40caf2-cc}

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 6
支持
分享
最新回复 (4)
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
666
2025-1-14 18:35
0
雪    币: 228
活跃值: (180)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
2025-1-14 18:43
0
雪    币: 229
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4

最后于 2025-1-14 18:45 被inf_编辑 ,原因:
2025-1-14 18:45
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
2025-1-16 15:20
0
游客
登录 | 注册 方可回帖
返回