5.6 汇编语言:汇编高效数组寻址

简介: 数组和指针都是用来处理内存地址的操作,二者在C语言中可以互换使用。数组是相同数据类型的一组集合,这些数据在内存中是连续存储的,在C语言中可以定义一维、二维、甚至多维数组。多维数组在内存中也是连续存储的,只是数据的组织方式不同。在汇编语言中,实现多维数组的寻址方式相对于C语言来说稍显复杂,但仍然可行。下面介绍一些常用的汇编语言方式来实现多维数组的寻址。

数组和指针都是用来处理内存地址的操作,二者在C语言中可以互换使用。数组是相同数据类型的一组集合,这些数据在内存中是连续存储的,在C语言中可以定义一维、二维、甚至多维数组。多维数组在内存中也是连续存储的,只是数据的组织方式不同。在汇编语言中,实现多维数组的寻址方式相对于C语言来说稍显复杂,但仍然可行。下面介绍一些常用的汇编语言方式来实现多维数组的寻址。

6.1 数组取值操作

数组取值操作是实现数组寻址的基础,在汇编语言中取值的操作有多种实现方式,这里笔者准备了一个通用案例该案例中包含了,使用OFFSET,PTR,LENGTHOF,TYPE,SIZEOF依次取值的操作细节,读者可自行编译并观察程序的取值过程并以此熟悉这些常用汇编指令集的使用。

  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
  WordVar1 WORD 1234h
  DwordVar2 DWORD 12345678h

  ArrayBT BYTE 1,2,3,4,5,6,7,8,9,0h
  ArrayDW DWORD 1000,2000,3000,4000,5000,6000,7000,8000,9000,0h
  ArrayTP DWORD 30 DUP(?)
.code

  main PROC
    ; 使用 OFFSET 可返回数据标号的偏移地址,单位是字节.
    ; 偏移地址代表标号距DS数据段基址的距离.
    xor eax,eax
    mov eax,offset WordVar1
    mov eax,offset DwordVar2

    ; 使用 PTR 可指定默认取出参数的大小(DWORD/WORD/BYTE)
    mov eax,dword ptr ds:[DwordVar2]     ; eax = 12345678h
    xor eax,eax
    mov ax,word ptr ds:[DwordVar2]       ; ax = 5678h
    mov ax,word ptr ds:[DwordVar2 + 2]   ; ax = 1234h

    ; 使用 LENGTHOF 可以计算数组元素的数量
    xor eax,eax
    mov eax,lengthof ArrayDW             ; eax = 10
    mov eax,lengthof ArrayBT             ; eax = 10

    ; 使用 TYPE 可返回按照字节计算的单个元素的大小.
    xor eax,eax
    mov eax,TYPE WordVar1                ; eax = 2
    mov eax,TYPE DwordVar2               ; eax = 4
    mov eax,TYPE ArrayDW                 ; eax = 4

    ; 使用 SIZEOF 返回等于LENGTHOF(总元素数)和TYPE(每个元素占用字节)返回值的乘基.
    xor eax,eax
    mov eax,sizeof ArrayBT               ; eax = 10
    mov eax,sizeof ArrayTP               ; eax = 120

    invoke ExitProcess,0
  main ENDP
END main

6.2 数组直接寻址

在声明变量名称的后面加上偏移地址即可实现直接寻址,直接寻址中可以通过立即数寻址,也可以通过寄存器相加的方式寻址,如果遇到双字等还可以使用基址变址寻址,这些寻址都属于直接寻址.

  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
  ArrayB BYTE 10h,20h,30h,40h,50h
  ArrayW WORD 100h,200h,300h,400h
  ArrayDW DWORD 1h,2h,3h,4h,5h,6h,7h,8h,9h
.code
  main PROC
    ; 针对字节的寻址操作
    mov al,[ArrayB]           ; al=10
    mov al,[ArrayB+1]         ; al=20
    mov al,[ArrayB+2]         ; al=30

    ; 针对内存单元字存储操作
    mov bx,[ArrayW]           ; bx=100
    mov bx,[ArrayW+2]         ; bx=200
    mov bx,[ArrayW+4]         ; bx=300

    ; 针对内存单元双字存储操作
    mov eax,[ArrayDW]         ; eax=00000001
    mov eax,[ArrayDW+4]       ; eax=00000002
    mov eax,[ArrayDW+8]       ; eax=00000003

    ; 基址加偏移寻址: 通过循环eax的值进行寻址,每次eax递增2
    mov esi,offset ArrayW
    mov eax,0
    mov ecx,lengthof ArrayW
  s1:
    mov dx,word ptr ds:[esi + eax]
    add eax,2
    loop s1

    ; 基址变址寻址: 循环取出数组中的元素
    mov esi,offset ArrayDW                 ; 数组基址
    mov eax,0                              ; 定义为元素下标
    mov ecx,lengthof ArrayDW               ; 循环次数
  s2:
    mov edi,dword ptr ds:[esi + eax * 4]   ; 取出数值放入edi
    inc eax                                ; 数组递增
    loop s2

    invoke ExitProcess,0
  main ENDP
END main

6.3 数组间接寻址

数组中没有固定的编号,处理此类数组唯一可行的方法是用寄存器作为指针并操作寄存器的值,这种方法称为间接寻址,间接寻址通常可通过ESI实现内存寻址,也可通过ESP实现对堆栈的寻址操作.

  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
  ArrayDW DWORD 1h,2h,3h,4h,5h,6h,7h,8h,9h
.code
  main PROC
    ; 第一种: 通过使用ESI寄存器实现寻址.
    mov esi,offset ArrayDW               ; 取出数组基地址
    mov ecx,lengthof ArrayDW             ; 取出数组元素个数
  s1:
    mov eax,dword ptr ds:[esi]           ; 间接寻址
    add esi,4                            ; 每次递增4
    loop s1

    ; 第二种: 通过ESP堆栈寄存器,实现寻址.
    mov eax,100                ; eax=1
    mov ebx,200                ; ebx=2
    mov ecx,300                ; ecx=3
    push eax                   ; push 1
    push ebx                   ; push 2
    push ecx                   ; push 3

    mov edx,[esp + 8]          ; EDX = [ESP+8] = 1
    mov edx,[esp + 4]          ; EDX = [ESP+4] = 2 
    mov edx,[esp]              ; EDX = [ESP] = 3

    ; 第三种(高级版): 通过ESP堆栈寄存器,实现寻址.
    push ebp
    mov ebp,esp                      ; 保存栈地址
    lea eax,dword ptr ds:[ArrayDW]   ; 获取到ArrayDW基地址
    ; -> 先将数据压栈
    mov ecx,9                        ; 循环9次
  s2: push dword ptr ss:[eax]          ; 将数据压入堆栈
    add eax,4                        ; 每次递增4字节
    loop s2
    ; -> 在堆栈中取数据
    mov eax,32                       ; 此处是 4*9=36 36 - 4 = 32
    mov ecx,9                        ; 循环9次
  s3: mov edx,dword ptr ss:[esp + eax] ; 寻找栈中元素
    sub eax,4                        ; 每次递减4字节
    loop s3

    add esp,36               ; 用完之后修正堆栈
    pop ebp                  ; 恢复ebp

    invoke ExitProcess,0
  main ENDP
END main

6.4 比例因子寻址

比例因子寻址是一种常见的寻址方式,通常用于访问数组、矩阵等数据结构。通过指定不同的比例因子,可以实现对多维数组的访问。在使用比例因子寻址时,需要考虑变量的偏移地址、维度、类型以及访问方式等因素,另外比例因子寻址的效率通常比直接寻址要低,因为需要进行一些额外的乘法和加法运算。

使用比例因子寻址可以方便地访问数组或结构体中的元素。在汇编语言中,比例因子可以通过指定一个乘数来实现,这个乘数可以是1、2、4或8,它定义了一个元素相对于数组起始地址的偏移量。

以下例子每个DWORD=4字节,且总元素下标=0-3,得出比例因子3* type arrayDW,并根据比例因子实现对数组的寻址操作。

  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
  ArrayW  WORD  1h,2h,3h,4h,5h
  ArrayDW DWORD 1h,2h,3h,4h,5h,6h,7h,8h,9h

  TwoArray DWORD 10h,20h,30h,40h,50h
  RowSize = ($ - TwoArray)            ; 每行所占空间 20 字节
     DWORD 60h,70h,80h,90h,0ah
     DWORD 0bh,0ch,0dh,0eh,0fh
.code
  main PROC

    ; 第一种比例因子寻址
    mov esi,0                 ; 初始化因子
    mov ecx,9                 ; 设置循环次数
  s1:
    mov eax,ArrayDW[esi * 4]  ; 通过因子寻址,4 = DWORD
    add esi,1                 ; 递增因子
    loop s1

    ; 第二种比例因子寻址
    mov esi,0
    lea edi,word ptr ds:[ArrayW]
    mov ecx,5
  s2:
    mov ax,word ptr ds:[edi + esi * type ArrayW]
    inc esi
    loop s2

    ; 第三种二维数组寻址
    row_index = 1
    column_index = 2

    mov ebx,offset TwoArray            ; 数组首地址
    add ebx,RowSize * row_index        ; 控制寻址行
    mov esi,column_index               ; 控制行中第几个
    mov eax, dword ptr ds:[ebx + esi * TYPE TwoArray]

    invoke ExitProcess,0
  main ENDP
END main

以二维数组为例,通过比例因子寻址可以模拟实现二维数组寻址操作。比例因子是指访问数组元素时,相邻元素之间在内存中的跨度。在访问二维数组时,需要指定两个比例因子:第一个比例因子表示行数,第二个比例因子表示列数。

  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
  TwoArray DWORD 10h,20h,30h,40h,50h
  RowSize = ($ - TwoArray)            ; 每行所占空间 20 字节
     DWORD 60h,70h,80h,90h,0ah
     DWORD 0bh,0ch,0dh,0eh,0fh
.code
  main PROC
    lea esi,dword ptr ds:[TwoArray]  ; 取基地址
    mov eax,0                        ; 控制外层循环变量
    mov ecx,3                        ; 外层循环次数
  s1:
    push ecx                         ; 保存外循环次数
    push eax

    mov ecx,5                        ; 内层循环数
  s2: add eax,4                        ; 每次递增4
    mov edx,dword ptr ds:[esi + eax] ; 定位到内层循环元素
    loop s2

    pop eax
    pop ecx
    add eax,20                       ; 控制外层数组
    loop s1 

    invoke ExitProcess,0
  main ENDP
END main

通过使用比例因子的方式可以对数组进行求和。一般来说,数组求和可以使用循环语句来实现,但在某些情况下,可以通过使用比例因子的方式来提高求和的效率。

在使用比例因子求和时,需要使用汇编指令lea和add。首先,使用lea指令计算出数组元素的地址,然后使用add指令求出数组元素的和。例如,假设有一个100个元素的整型数组a,可以使用以下汇编指令来计算数组元素的和:

  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
  ArrayA DWORD 10h,20h,30h,40h,50h
  ArrayB DWORD 10h,20h,30h,40h,50h
  NewArray DWORD 5 dup(0)
.code
  main PROC
    ; 循环让数组中的每一个数加10后回写
    mov ebx,0
    mov ecx,5
  s1:
    mov eax,dword ptr ds:[ArrayA + ebx * 4]
    add eax,10
    mov dword ptr ds:[ArrayA + ebx * 4],eax
    inc ebx
    loop s1

    ; 循环让数组A与数组B相加后赋值到数组NewArray
    mov ebx,0
    mov ecx,5
  s2:
    mov esi,dword ptr ds:[ArrayA + ebx]
    add esi,dword ptr ds:[ArrayB + ebx]
    mov dword ptr ds:[NewArray + ebx],esi
    add ebx,4
    loop s2

    invoke ExitProcess,0
  main ENDP
END main

6.5 数组指针寻址

指针变量是指存储另一个变量的地址的变量。指针类型是指可以存储对另一个变量的指针的数据类型。在Intel处理器中,涉及指针时有near指针和far指针两种不同类型,其中Far指针一般用于实模式下的内存管理,而在保护模式下,一般采用Near指针。

在保护模式下,Near指针指的是一个指针变量,它只存储一个内存地址。通常,Near指针的大小为4字节,因此,它可以被存储在单个双字变量中。除此之外,也可以使用void*类型的指针来代表一个指向任何类型的指针。

数组指针是指一个指向数组的指针变量。数组名是数组第一个元素的地址。因此,对数组名求地址就是数组指针。数组指针可以进行地址的加减运算,从而实现对数组中不同元素的访问。

例如,假设有一个大小为10的整型数组a,可以使用以下汇编代码来访问其中一个元素(如a[3]):

lea esi, [a]             ; 将数组a的地址存储到esi中
mov eax, dword [esi+3*4] ; 将a[3]的值存储到eax中

在这个示例中,使用lea指令将数组a的地址存储到esi中。数组a元素的大小为4个字节(即eax大小),所以这里是使用3 * 4来表示a[3]的偏移地址。虽然这里的地址计算看起来比较繁琐,但是通过使用数组指针寻址,可以避免对数组进行循环访问等相对低效的操作。

  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
  ArrayA  WORD  1h,2h,3h,4h,5h
  ArrayB DWORD 1h,2h,3h,4h,5h

  PtrA DWORD offset ArrayA     ; 指针 PtrA --> ArrayA
  PtrB DWORD offset ArrayB     ; 指针 PTRB --> ArrayB
.code
  main PROC

    mov ebx,0            ; 寻址因子
    mov ecx,5            ; 循环次数
  s1:
    mov esi,dword ptr ds:[PtrA]          ; 将指针指向PtrA
    mov ax,word ptr ds:[esi + ebx * 2]   ; 每次递增2字节

    mov esi,dword ptr ds:[PtrB]          ; 将指针指向PtrB
    mov eax,dword ptr cs:[esi + ebx * 4] ; 每次递增4字节
    inc esi              ; 基地址递增
    inc ebx              ; 因子递增
    loop s1

    invoke ExitProcess,0
  main ENDP
END main

6.6 模拟二维数组寻址

在汇编语言中,内存是线性的,只有一个维度,因此,二维数组需要通过模拟方式来实现。常用的方式是使用比例因子寻址和数组指针寻址。以比例因子寻址为例,可以使用汇编指令leamov来模拟实现二维数组的寻址操作。例如,假设有一个二维数组a[3][4],可以使用以下汇编指令来访问数组元素:

mov eax, [a + ebx * 4 + ecx * 4 * 3] ; 访问a[ebx][ecx]元素

其中,a是数组的基地址,ebx是列号,ecx是行号。指定一个比例因子为3,可以将二维数组转换成一维数组,每行的大小为4个字节,因此在访问a[ebx][ecx]时,需要加上行号的偏移量(即ecx * 4 * 3)。

除了使用比例因子寻址,还可以使用数组指针寻址来模拟二维数组的操作。例如,假设有一个二维数组b[3][4],可以使用以下汇编指令来访问数组元素:

lea esi, [b] ; 将数组b的地址存储到esi中
mov eax, dword ptr [esi + ebx * 16 + ecx * 4] ; 访问a[ebx][ecx]元素

在这个示例中,使用lea指令将二维数组b的地址存储到esi中。首先,指针+偏移,将现在想要查的数字所在的行号+列号的位置指向到了数组中,再通过mov指令将数组元素的值存储到eax中。

由于我们的内存本身就是线性的,所以C语言中的二维数组也是线性的,二维数组仅仅只是一维数组的高阶抽象,唯一的区别仅仅只是寻址方式的不同,首先我们先来在Debug模式下编译一段代码,然后分别分析一下C编译器是如何优化的。

void function_1()
{
  int array[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };
  int x = 0, y = 1;

  array[x][y] = 0;
}

void function_2()
{
  int array[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };
  int x = 0, y = 1;
  array[x][y] = 0;

  int a = 1, b = 2;
  array[a][b] = 1;
}

编译通过后,我们反汇编function_1函数,这段代码主要实现给array[0][1]赋值,核心代码如下:

0040106E    8B45 E4                 mov     eax, dword ptr [ebp-1C]        ; eax = x 坐标
00401071    6BC0 0C                 imul    eax, eax, 0C                   ; eax = x * 0c 索引数组
00401074    8D4C05 E8               lea     ecx, dword ptr [ebp+eax-18]    ; ecx = y 坐标
00401078    8B55 E0                 mov     edx, dword ptr [ebp-20]        ; edx = 1 二维维度
0040107B    C70491 00000000         mov     dword ptr [ecx+edx*4], 0       ; 1+1*4=5 4字节中的5,指向第2个元素

接着来解释一下上方汇编代码:

  • 1.第1条代码: 寄存器EAX是获取到的x的值,此处为C语言中的x=0
  • 2.第2条代码: 其中0C代表一个维度的长度,每个数组有3个元素(3x4=0C)每个元素4字节
  • 3.第3条代码: 寄存器ECX代表数组的y坐标
  • 4.第5条代码: 公式ecx + edx * 4相当于数组首地址 + sizeof(int) * y

寻址公式可总结为: 数组首地址 + sizeof(type[一维数组元素]) * x + sizeof(int) * y 简化后变成数组首地址 + x坐标 + (y坐标 * 4)即可得到寻址地址.

我们来编译function_2函数,一维数组的总大小3*4=0C,并通过寻址公式计算下.

004113F8 | C745 D8 00000000         | mov dword ptr ss:[ebp-0x28],0x0             | x = 0
004113FF | C745 CC 01000000         | mov dword ptr ss:[ebp-0x34],0x1             | y = 1
00411406 | 6B45 D8 0C               | imul eax,dword ptr ss:[ebp-0x28],0xC        | eax = x坐标
0041140A | 8D4C05 E4                | lea ecx,dword ptr ss:[ebp+eax-0x1C]         | ecx = 数组array[0]首地址
0041140E | 8B55 CC                  | mov edx,dword ptr ss:[ebp-0x34]             | edx = y坐标
00411411 | C70491 00000000          | mov dword ptr ds:[ecx+edx*4],0x0            | ecx(数组首地址) + y坐标 * 4

00411418 | C745 C0 01000000         | mov dword ptr ss:[ebp-0x40],0x1             | a = 1
0041141F | C745 B4 02000000         | mov dword ptr ss:[ebp-0x4C],0x2             | b = 2
00411426 | 6B45 C0 0C               | imul eax,dword ptr ss:[ebp-0x40],0xC        | eax = 1 * 0c = 0c
0041142A | 8D4C05 E4                | lea ecx,dword ptr ss:[ebp+eax-0x1C]         | 找到数组array[1]的首地址
0041142E | 8B55 B4                  | mov edx,dword ptr ss:[ebp-0x4C]             | 数组b坐标 2
00411431 | C70491 01000000          | mov dword ptr ds:[ecx+edx*4],0x1            | ecx(数组首地址) + b坐标 * 4

根据分析结论,我自己仿照编译器编译特性,仿写了一段汇编版寻址代码,代码很简单,如下:

    .386p
    .model flat,stdcall
    option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
  MyArrayDWORD DWORD 1,2,3,4,5,6,0h
  MyArrayWORD DWORD 1,2,3,4,5,6,7,8,9,10,0h
.code
    main PROC
      xor eax,eax
      xor ebx,ebx
      xor ecx,ecx
      xor edx,edx

      ; 模拟实现对二维4字节数组寻址 寻找 MyArrayDWORD[1][1]
      ; int array[2][3] = {
  {1,2,3},{4,5,6}}
      mov eax,0ch         ; 代表每个一维数组长度
      imul ebx,eax,1      ; 定位维度

      mov ecx,4           ; 每个四字节
      imul edx,ecx,1      ; 定位数组

      add ebx,edx         ; 累加步长

      mov edx,dword ptr [MyArrayDWORD + ebx]

      ; 模拟实现对二维数组寻址 寻找 MyArrayWORD[1][2]
      ; word array[2][5]={
  {1,2,3,4,5},{6,7,8,9,10}}
      xor eax,eax
      xor ebx,ebx
      xor ecx,ecx
      xor edx,edx

      mov eax,14h        ; 每个一维长度 4 * 5
      imul ebx,eax,1     ; 定位到 {6,7,8,9,10}

      mov ecx,4          ; 定义步长4字节
      imul edx,ecx,2     ; 定位到元素 8

      add ebx,edx        ; 累加步长

      mov edx,dword ptr [MyArrayWORD + ebx]

    main ENDP
END main

6.7 模拟三维数组寻址

相对于二维数组,三维数组的寻址更加繁琐,但仍然可以使用类似的方式进行模拟。常用的方式是使用比例因子寻址和多级指针。以比例因子寻址为例,我们可以使用数组指针来模拟多维数组的访问操作。假设有一个三维数组c[2][3][4],可以使用以下汇编指令来访问数组元素:

lea esi, [c] ; 将数组c的地址存储到esi中
mov eax, dword ptr [esi + (i*3+j)*16 + k*4] ; 访问c[i][j][k]元素

其中,i表示数组的第一维下标,j表示数组的第二维下标,k表示数组的第三维下标。指定一个比例因子为16,可以将三维数组转换成一维数组,每行的大小为4 * 4 = 16字节,因此在访问c[i][j][k]时,需要加上前两个维度的偏移量(即(i*3+j) * 16),再加上第三个维度的偏移量(即k * 4)。

除了使用比例因子寻址,还可以使用多级指针来模拟三维数组的访问操作。例如,假设有一个三维数组d[2][3][4],可以使用以下汇编指令来访问数组元素:

lea eax, [d] ; 将数组d的地址存储到eax中
mov ebx, [eax + i*4] ; 获取指向d[i]的指针
mov ecx, [ebx + j*4] ; 获取指向d[i][j]的指针
mov edx, [ecx + k*4] ; 获取d[i][j][k]的值

在这个示例中,使用lea指令将三维数组d的地址存储到eax中。然后,使用mov指令依次获取d[i]、d[i][j]以及d[i][j][k]的指针并获取其值。其中,i表示数组的第一维下标,j表示数组的第二维下标,k表示数组的第三维下标。

老样子,我们先来编写一段代码,代码中只需要声明一个三维数组即可.

int main(int argc, char* argv[])
{
    // int Array[M][C][H]
    int Array[2][3][4] = {NULL};
    int x = 0;
    int y = 1;
    int z = 2;

    Array[x][y][z] = 3;
    return 0;
}

首先我们反汇编这段代码,然后观察反汇编代码展示形式,并套入公式看看.针对三维数组 int Array[M][C][H]其下标操Array[x][y][z]=3

  • 寻址公式为: Array + sizeof(type[C][H]) * x + sizeof(type[H])*y + sizeof(type)*z
  • 寻址公式为: Array + sizeof(Array[C][H]) * x + sizeof(Array[H]) * y + sizeof(Array[M]) * z
00401056  |.  8B45 9C       mov     eax, dword ptr [ebp-64]      ; eax = x
00401059  |.  6BC0 30       imul    eax, eax, 30                 ; sizeof(type[C][H]) * x
0040105C  |.  8D4C05 A0     lea     ecx, dword ptr [ebp+eax-60]  ; 取Array[C][H]基地址
00401060  |.  8B55 98       mov     edx, dword ptr [ebp-68]      ; Array[C]
00401063  |.  C1E2 04       shl     edx, 4                       ;
00401066  |.  03CA          add     ecx, edx                     ; 
00401068  |.  8B45 94       mov     eax, dword ptr [ebp-6C]      ; Array[Z]
0040106B  |.  C70481 030000 mov     dword ptr [ecx+eax*4], 3

接着来解释一下上方汇编代码:

  • 1.第1条指令: 得出eax=x的值.
  • 2.第2条指令: 其中eax * 30,相当于求出sizeof(type[C][H]) * x
  • 3.第3条指令: 求出数组首地址+eax-60也就求出Array[H]位置,并取地址放入ECX
  • 4.第4条指令: 临时[ebp-68]存放Y的值,此处就是得到y的值
  • 5.第5条指令: 左移4位,相当于2^4次方也就是16这一步相当于求sizeof(type[H])的值
  • 6.最后Array[M] + sizeof(type[H])的值求出Array[M][C]的值

接下来我们通过汇编的方式来实现这个寻址过程,为了方便理解,先来写一段C代码,代码中实现定位Array[1][2][3]的位置.

int main(int argc, char* argv[])
{
  // 对应关系: Array[M][C][H]
  int Array[2][3][4] = 
  {
    {
      { 1, 2, 3, 4 }, { 2, 3, 4, 5 }, { 3, 4, 5, 6 }
    },
    {
      { 4, 5, 6, 7 }, { 5, 6, 7, 8 }, { 6, 7, 8, 9 }
    }
  };

  int x = 1;
  int y = 2;
  int z = 3;

  Array[x][y][z] = 999;

  return 0;
}

最终的汇编版如下,这段代码我一开始并没有想出来怎么写,经过不断尝试,终于算是理解了它的寻址方式,并成功实现了仿写,除去此种方式外其实可以完全将imul替换为shl这样还可以提高运算效率.

    .386p
    .model flat,stdcall
    option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
  MyArray DWORD 1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7,5,6,7,8,6,7,8,9,0h
  Count DWORD ?
  x DWORD ?
  y DWORD ?
  z DWORD ?

.code
    main PROC
      xor eax,eax
      xor ebx,ebx
      xor ecx,ecx
      xor edx,edx

      ; 定位 Array[1][2][3]
      mov dword ptr [x],1h
      mov dword ptr [y],2h
      mov dword ptr [z],3h

      ; 找到 Array[M]
      imul eax,dword ptr [x],30h               ; 定位 Array[1] => ([C] * [H]) * 4
      lea ecx,dword ptr [MyArray + eax]        ; 定位 Array[1] 基地址

      ; 找到 Array[C]
      mov ebx,dword ptr [y]                    ; 定位 Array[2] => ([C])
      shl ebx,4h                               ; 2^4=32 计算 (EBX * 16)
      add ecx,ebx                              ; Array[M] + Array[C]

      ; 找到 Array[H]
      imul edx,dword ptr[z],4h                 ; Array[H] * 4
      add ecx,edx

      mov dword ptr [Count],ecx                ; 取出结果

    main ENDP
END main

本文作者: 王瑞
本文链接: https://www.lyshark.com/post/a38e7460.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

目录
相关文章
|
9月前
|
存储 安全 编译器
5.10 汇编语言:汇编过程与结构
过程的实现离不开堆栈的应用,堆栈是一种后进先出`(LIFO)`的数据结构,最后压入栈的值总是最先被弹出,而新数值在执行压栈时总是被压入到栈的最顶端,栈主要功能是暂时存放数据和地址,通常用来保护断点和现场。栈是由`CPU`管理的线性内存数组,它使用两个寄存器`(SS和ESP)`来保存栈的状态,SS寄存器存放段选择符,而ESP寄存器的值通常是指向特定位置的一个32位偏移值,我们很少需要直接操作ESP寄存器,相反的ESP寄存器总是由`CALL,RET,PUSH,POP`等这类指令间接性的修改。
42 0
|
9月前
|
存储 算法 编译器
5.8 汇编语言:汇编高效除法运算
通常情况下计算除法会使用`div/idiv`这两条指令,该指令分别用于计算无符号和有符号除法运算,但除法运算所需要耗费的时间非常多,大概需要比乘法运算多消耗10倍的CPU时钟,在Debug模式下,除法运算不会被优化,但Release模式下,除法运算指令会被特定的算法经过优化后转化为为乘法,这样就可以提高除法运算的效率。
66 0
|
9月前
|
编译器
5.7 汇编语言:汇编高效乘法运算
乘法指令是一种在CPU中实现的基本算术操作,用于计算两个数的乘积。在汇编语言中,乘法指令通常是通过`mul(无符号乘法)`和`imul(有符号乘法)`这两个指令实现的。由于乘法指令在执行时所消耗的时钟周期较多,所以编译器在优化代码时通常会尝试将乘法操作转换为更高效的加法、和移位操作。
100 0
|
12月前
【8086汇编】《汇编语言(第三版)》实验一
需要用到的指令✨✨ 查看、修改CPU中寄存器的内容:R命令 查看内存中的内容:D命令 修改内存中的内容:E命令(可以写入数据、指令,在内存中,它们实际上没有区别) 将内存中的内容解释为机器指令和对应的汇编指令:U命令 执行CS:IP指向的内存单元处的指令:T命令 以汇编指令的形式向内存中写入指令:A命令
|
Android开发
【Android 逆向】arm 汇编 ( 使用 IDA 解析 arm 架构的动态库文件 | 分析 malloc 函数的 arm 汇编语言 )
【Android 逆向】arm 汇编 ( 使用 IDA 解析 arm 架构的动态库文件 | 分析 malloc 函数的 arm 汇编语言 )
301 0
【Android 逆向】arm 汇编 ( 使用 IDA 解析 arm 架构的动态库文件 | 分析 malloc 函数的 arm 汇编语言 )
|
Android开发
【Android 逆向】x86 汇编 ( 使用 IDA 解析 x86 架构的动态库文件 | x86 汇编语言分析 )(二)
【Android 逆向】x86 汇编 ( 使用 IDA 解析 x86 架构的动态库文件 | x86 汇编语言分析 )(二)
127 0
【Android 逆向】x86 汇编 ( 使用 IDA 解析 x86 架构的动态库文件 | x86 汇编语言分析 )(二)
|
开发工具 Android开发
【Android 逆向】x86 汇编 ( 使用 IDA 解析 x86 架构的动态库文件 | x86 汇编语言分析 )(一)
【Android 逆向】x86 汇编 ( 使用 IDA 解析 x86 架构的动态库文件 | x86 汇编语言分析 )(一)
126 0
【Android 逆向】x86 汇编 ( 使用 IDA 解析 x86 架构的动态库文件 | x86 汇编语言分析 )(一)
汇编(一) 汇编语言CPU、存储器、指令等概念
汇编、汇编语言、CPU、汇编指令、存储器
1817 0
【汇编语言/底层开发】4、使用masm编译、链接汇编源文件
0、准备工作: 由于现在我们的操作系统通常都是win7或者win8,所以要运行masm需要做一些准备工作。这里只简单描述一下过程,详细的请搜索这篇文章《Win7(64位)下使用MASM和DEBUG方法》。
1098 0
|
2月前
|
存储 自然语言处理 编译器
编译和链接(翻译环境:预编译+编译+汇编+链接​、运行环境)
编译和链接(翻译环境:预编译+编译+汇编+链接​、运行环境)