5.10 彙編語言:彙編過程與結構

来源:https://www.cnblogs.com/LyShark/archive/2023/08/24/17653301.html
-Advertisement-
Play Games

過程的實現離不開堆棧的應用,堆棧是一種後進先出`(LIFO)`的數據結構,最後壓入棧的值總是最先被彈出,而新數值在執行壓棧時總是被壓入到棧的最頂端,棧主要功能是暫時存放數據和地址,通常用來保護斷點和現場。棧是由`CPU`管理的線性記憶體數組,它使用兩個寄存器`(SS和ESP)`來保存棧的狀態,SS寄存... ...


過程的實現離不開堆棧的應用,堆棧是一種後進先出(LIFO)的數據結構,最後壓入棧的值總是最先被彈出,而新數值在執行壓棧時總是被壓入到棧的最頂端,棧主要功能是暫時存放數據和地址,通常用來保護斷點和現場。

棧是由CPU管理的線性記憶體數組,它使用兩個寄存器(SS和ESP)來保存棧的狀態,SS寄存器存放段選擇符,而ESP寄存器的值通常是指向特定位置的一個32位偏移值,我們很少需要直接操作ESP寄存器,相反的ESP寄存器總是由CALL,RET,PUSH,POP等這類指令間接性的修改。

CPU提供了兩個特殊的寄存器用於標識位於系統棧頂端的棧幀。

  • ESP 棧指針寄存器:棧指針寄存器,其記憶體放著一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂。
  • EBP 基址指針寄存器:基址指針寄存器,其記憶體放著一個指針,該指針永遠指向系統棧最上面一個棧幀的底部。

在通常情況下ESP是可變的,隨著棧的生成而逐漸變小,而EBP寄存器是固定的,只有當函數的調用後,發生入棧操作而改變。

  • 執行PUSH壓棧時,堆棧指針自動減4,再將壓棧的值複製到堆棧指針所指向的記憶體地址。
  • 執行POP出棧時,從棧頂移走一個值並將其複製給記憶體或寄存器,然後再將堆棧指針自動加4。
  • 執行CALL調用時,CPU會用堆棧保存當前被調用過程的返回地址,直到遇到RET指令再將其彈出。

10.1 PUSH/POP

PUSH和POP是彙編語言中用於堆棧操作的指令,它們通常用於保存和恢復寄存器的值,參數傳遞和函數調用等。

PUSH指令用於將操作數壓入堆棧中,它執行的操作包括將操作數複製到堆棧的棧頂,並將堆棧指針(ESP)減去相應的位元組數。指令格式如下:

PUSH operand

其中,operand可以是8位,16位或32位的寄存器,立即數,以及記憶體中的某個值。例如,要將寄存器EAX的值壓入堆棧中,可以使用以下指令:

PUSH EAX

從彙編代碼的角度來看,PUSH指令將操作數存儲到堆棧中,它實際上是一個入棧操作。

POP指令用於將堆棧中棧頂的值彈出到指定的目的操作數中,它執行的操作包括將堆棧頂部的值移動到指定的操作數,並將堆棧指針增加相應的位元組數。指令格式如下:

POP operand

其中,operand可以是8位,16位或32位的寄存器,立即數,以及記憶體中的某個位置。例如,要將從堆棧中彈出的值存儲到BX寄存器中,可以使用以下指令:

POP EBX

從彙編代碼的角度來看,POP指令將從堆棧中取出一個值,並將其存儲到目的操作數中,它是一個出棧操作。

在函數調用時,PUSH指令被用於向堆棧中推送函數的參數,這些參數可以是寄存器、立即數或者記憶體中的某個值。在函數返回之前,POP指令被用於將堆棧頂部的值彈出,並將其存儲到寄存器或者記憶體中。

讀者需要特別註意,在使用PUSHPOP指令時需要保證堆棧的平衡,也就是說,每個PUSH指令必須有對應的POP指令,否則堆棧會失去平衡,最終導致程式出現錯誤。

在讀者瞭解了這兩條指令時則可以執行一些特殊的操作,如下代碼我們以數組入棧與出棧為例,執行PUSH指令時,首先減小ESP的值,然後把源操作數複製到堆棧上,執行POP指令則是先將數據彈出到目的操作數中,然後再執行ESP值增加4,並以此分別將數組中的元素壓入棧,最終再通過POP將元素反彈出來。

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

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

.data
  Array DWORD 1,2,3,4,5,6,7,8,9,10
  szFmt BYTE '%d ',0dh,0ah,0
.code
  main PROC
    ; 使用Push指令將數組正向入棧
    mov eax,0
    mov ecx,10
  S1:
    push dword ptr ds:[Array + eax * 4]
    inc eax
    loop S1
    
    ; 使用pop指令將數組反向彈出
    mov ecx,10
  S2:
    push ecx                         ; 保護ecx
    pop ebx                          ; 將Array數組元素彈出到ebx
    invoke crt_printf,addr szFmt,ebx
    pop ecx                          ; 彈出ecx
    loop S2
    
    int 3
  main ENDP
END main

至此當讀者理解了這兩個指令之後,那麼利用堆棧的先進後出特定,我們就可以實現將特殊的字元串反轉後輸出的效果,首先我們迴圈將字元串壓入堆棧,然後再從堆棧中反向彈出來,這樣就可以實現字元串的反轉操作,這段代碼的實現也相對較為容易;

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

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

.data
  MyString BYTE "hello lyshark",0
  NameSize DWORD ($ - MyString) - 1
  szFmt BYTE '%s',0dh,0ah,0
.code
  main PROC
    ; 正向壓入字元串
    mov ecx,dword ptr ds:[NameSize]
    mov esi,0
  S1: movzx eax,byte ptr ds:[MyString + esi]
    push eax
    inc esi
    loop S1

    ; 反向彈出字元串
    mov ecx,dword ptr ds:[NameSize]
    mov esi,0
  S2: pop eax
    mov byte ptr ds:[MyString + esi],al
    inc esi
    loop S2
    
    invoke crt_printf,addr szFmt,addr MyString
    int 3
  main ENDP
END main

10.2 PROC/ENDP

PROC/ENDP 偽指令是用於定義過程(函數)的偽指令,這兩個偽指令可分別定義過程的開始和結束位置。此處讀者需要註意,這兩條偽指令並非是彙編語言中所相容的,而是MASM編譯器為我們提供的一個巨集,是MASM的一部分,它允許程式員使用彙編語言定義過程(函數)可以像標準彙編指令一樣使用。

對於不使用巨集定義來創建函數時我們通常會自己管理函數棧參數,而有了巨集定義這些功能都可交給編譯器去管理,下麵的一個案例中,我們通過使用過程創建ArraySum函數,實現對整數數組求和操作,函數預設將返回值存儲在EAX中,並列印輸出求和後的參數。

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

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

.data
  MyArray  DWORD 1,2,3,4,5,6,7,8,9,10
  Sum      DWORD ?
  szFmt    BYTE '%d',0dh,0ah,0
.code
  ; 數組求和過程
  ArraySum PROC
    push esi                     ; 保存ESI,ECX
    push ecx
    xor eax,eax
    
  S1: add eax,dword ptr ds:[esi]   ; 取值並相加
    add esi,4                    ; 遞增數組指針
    loop S1
    pop ecx                      ; 恢復ESI,ECX
    pop esi
    ret
  ArraySum endp

  main PROC
    lea esi,dword ptr ds:[MyArray]   ; 取出數組基址
    mov ecx,lengthof MyArray         ; 取出元素數目
    call ArraySum                    ; 調用方法
    mov dword ptr ds:[Sum],eax       ; 得到結果
    invoke crt_printf,addr szFmt,Sum
    int 3
  main ENDP
END main

接著我們來實現一個具有獲取隨機數功能的案例,在C語言中如果需要獲得一個隨機數一般會調用Seed函數,如果讀者逆向分析過這個函數的實現原理,那麼讀者應該能理解,在調用取隨機數之前會生成一個隨機數種子,這個隨機數種子的生成則依賴於0x343FDh這個特殊的常量地址,當我們每次訪問該地址都會產出一個隨機的數據,當得到該數據後,我們再通過除法運算取出溢出數據作為隨機數使用實現了該功能。

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

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

.data
  seed DWORD 1
  szFmt    BYTE '隨機數: %d',0dh,0ah,0
.code
  ; 生成 0 - FFFFFFFFh 的隨機種子
  Random32 PROC
    push  edx
    mov   eax, 343FDh
    imul  seed
    add   eax, 269EC3h
    mov   seed, eax
    ror   eax,8
    pop   edx
    ret
  Random32 endp
  
  ; 生成隨機數
  RandomRange PROC
    push  ebx
    push  edx
    
    mov   ebx,eax
    call  Random32
    mov   edx,0
    div   ebx
    mov   eax,edx

    pop   edx
    pop   ebx
    ret
  RandomRange endp

  main PROC
  
    ; 調用後取出隨機數
    call RandomRange
    invoke crt_printf,addr szFmt,eax
    int 3
  main ENDP
END main

10.3 局部參數傳遞

在彙編語言中,可以使用堆棧來傳遞函數參數和創建局部變數。當程式執行到函數調用語句時,需要將函數參數傳遞給被調用函數。為了實現參數傳遞,程式會將參數壓入棧中,然後調用被調用函數。被調用函數從棧中彈出參數並執行,然後將返回值存儲在寄存器中,最後通過跳轉返回到調用函數。

局部變數也可以通過在棧中分配記憶體來創建。在函數開始時,可以使用push指令將局部變數壓入棧中。在函數結束時,可以使用pop指令將變數從棧中彈出。由於棧是後進先出的數據結構,局部變數的創建可以很方便地通過在棧上壓入一些數據來實現。

局部變數是在程式運行時由系統動態的在棧上開闢的,在記憶體中通常在基址指針(EBP)之下,儘管在彙編時不能給定預設值,但可以在運行時初始化,如下一段C語言偽代碼:

void MySub()
{
  int var1 = 10;
  int var2 = 20;
}

上述的代碼經過C編譯後,會變成如下彙編指令,其中EBP-4必須是4的倍數,因為預設就是4位元組存儲,如果去掉了mov esp,ebp,那麼當執行pop ebp時將會得到EBP等於10,執行RET指令會導致控制轉移到記憶體地址10處執行,從而程式會崩潰。

MySub PROC
  push ebp                  ; 將EBP存儲在棧中
  mov ebp,esp               ; 堆棧框架的基址
  sub esp,8                 ; 創建局部變數空間(分配2個局部變數)

  mov DWORD PTR [ebp-8],10  ; var1 = 10
  mov DWORD PTR [ebp-4],20  ; var2 = 20

  mov esp,ebp               ; 從堆棧上刪除局部變數
  pop ebp                   ; 恢復EBP指針
  ret 8                     ; 返回,清理堆棧
MySub ENDP

為了使上述代碼片段更易於理解,可以在上述的代碼的基礎上給每個變數的引用地址都定義一個符號,併在代碼中使用這些符號,如下代碼所示,代碼中定義了一個名為MySub的過程,該過程將兩個局部變數分別設置為1020

在該過程中,首先使用push ebp指令將舊的基址指針壓入棧中,並將ESP寄存器的值存儲到ebp中。這個舊的基址指針將在函數執行完畢後被恢復。然後,我們使用sub esp,8指令將8位元組的空間分配給兩個局部變數。在堆棧上分配的空間可以通過var1_localvar2_local符號來訪問。在這裡,我們定義了兩個符號,將它們與ebp寄存器進行偏移以訪問這些局部變數。var1_local的地址為[ebp-8]var2_local的地址為[ebp-4]。然後,我們使用mov指令將1020分別存儲到這些局部變數中。最後,我們將ESP寄存器的值存儲回ebp中,並使用pop ebp指令將舊的基址指針彈出堆棧。現在,棧頂指針(ESP)下移恢覆上面分配的8個位元組的空間,最後通過ret 8返回到調用函數。

在使用堆棧傳參和創建局部變數時,需要謹慎考慮棧指針的位置,並確保遵守調用約定以確保正確地傳遞參數和返回值。

var1_local EQU DWORD PTR [ebp-8]   ; 添加符號1
var2_local EQU DWORD PTR [ebp-4]   ; 添加符號2

MySub PROC
  push ebp
  mov ebp,esp
  sub esp,8
  mov var1_local,10
  mov var2_local,20
  mov esp,ebp
  pop ebp
  ret 8
MySub ENDP

接著我們來實現一個具有功能的案例,首先為了能更好的讓讀者理解我們先使用C語言方式實現MakeArray()函數,該函數的內部是動態生成的一個MyString數組,並通過迴圈填充為星號字元串,最後使用POP彈出,並輸出結果,觀察後嘗試用彙編實現。

void makeArray()
{
  char MyString[30];
  for(int i=0;i<30;i++)
  {
    myString[i] = "*";
  }
}

call makeArray()

上述C語言代碼如果翻譯為彙編格式則如下所示,代碼使用彙編語言實現makeArray的程式,該程式開闢了一個長度為30的數組,將其中的元素填充為*,然後彈出兩個元素,並將它們輸出到控制台。

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

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

.data
  szFmt BYTE '出棧數據: %x ',0dh,0ah,0
.code
  makeArray PROC
    push ebp
    mov ebp,esp
    
    ; 開闢局部數組
    sub esp,32                    ; MyString基地址位於 [ebp - 30]
    lea esi,[ebp - 30]            ; 載入MyString的地址
    
    ; 填充數據
    mov ecx,30                    ; 迴圈計數
  S1: mov byte ptr ds:[esi],'*'     ; 填充為*
    inc esi                       ; 每次遞增一個位元組
    loop S1
    
    ; 彈出2個元素並輸出,出棧數據
    pop eax
    invoke crt_printf,addr szFmt,eax
    
    pop eax
    invoke crt_printf,addr szFmt,eax  
    
    ; 以下平棧,由於我們手動彈出了2個數據
    ; 則平棧 32 - (2 * 4) = 24 
    add esp,24                    ; 平棧
    mov esp,ebp
    pop ebp                       ; 恢復EBP
    ret
  makeArray endp

  main PROC
    call makeArray
    invoke ExitProcess,0
  main ENDP
END main

在該程式的開始部分,我們首先通過push ebpmov ebp,esp指令保存舊的基址指針並將當前棧頂指針(ESP)存儲到ebp中。然後,我們使用sub esp, 32指令開闢一個長度為30的數組MyString。我們將MyString數組的基地址存儲在[ebp - 30]的位置。使用lea esi, [ebp - 30]指令將MyString的基地址載入到esi寄存器中。該指令偏移ebp-30是因為ebp-4MakeArray函數的第一個參數的位置,因此需要增加四個位元組。我們利用MOV byte ptr ds:[esi],'*'指令將MyString中的所有元素填充為*

然後,使用pop eaxinvoke crt_printf, addr szFmt, eax指令兩次彈出兩個元素,並使用crt_printf函數輸出這些元素。該函數在msvcrt.dll庫中實現,用於將格式化的信息輸出到控制台。在輸出數據之後,我們通過add esp,24mov esp,ebp指令將堆棧平衡,恢複舊的基址指針ebp,然後從堆棧中彈出ebp,並通過ret指令返回到調用程式。


接著我們繼續來對比一下堆棧中參數傳遞的異同點,平棧的方式一般可分為調用者平棧和被調用者平棧,在使用堆棧傳參時,需要平衡棧以恢復之前的堆棧指針位置。

  • 當平棧由被調用者完成時,被調用函數使用ret指令將控制權返回到調用函數,並從堆棧中彈出返回地址。此時,被調用函數需要將之前分配的局部變數從堆棧中彈出,以便調用函數能夠恢復堆棧指針的位置。因此,被調用函數必須知道其在堆棧上分配的記憶體大小,並將該大小與其ret指令中的參數相匹配,以便調用函數可以正確恢復堆棧指針位置。

  • 當平棧由調用者完成時,調用函數需要在調用子函數之前平衡堆棧。因此,調用函數需要知道子函數在堆棧上分配的記憶體大小,併在調用子函數之前向堆棧提交額外的空間。調用函數可以使用add esp, N指令來恢復堆棧指針的位置,其中 N 是被調用函數在堆棧上分配的記憶體大小。然後,調用函數調用被調用函數,該函數將返回並將堆棧指針恢復到調用函數之前的位置。

如下這段彙編代碼中筆者分別實現了兩種調用方式,其中MyProcA函數是一種被調用者平棧,由於調用者並沒有堆棧修正所以需要在函數內部通過使用ret 12的方式平棧,之所以是12是因為我們使用了三個局部變數,而第二個MyProcB函數則是調用者平棧,該方式在函數內部並沒有返回任何參數,所以在調用函數結束後需要通過add esp,4的方式對堆棧進行修正。

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

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

.data
  szFmt BYTE '數據: %d ',0dh,0ah,0
.code
  ; 第一種方式:被調用者平棧
  MyProcA PROC
    push ebp
    mov ebp,esp
    
    xor eax,eax
    mov eax,dword ptr ss:[ebp + 16]   ; 獲取第一個參數
    mov ebx,dword ptr ss:[ebp + 12]   ; 獲取第二個參數
    mov ecx,dword ptr ss:[ebp + 8]    ; 獲取第三個參數
    
    add eax,ebx
    add eax,ebx
    add eax,ecx
    
    mov esp,ebp
    pop ebp
    ret 12       ; 此處ret12可平棧,也可使用 add ebp,12
  MyProcA endp

  ; 第二種方式:調用者平棧
  MyProcB PROC
    push ebp
    mov ebp,esp
    
    mov eax,dword ptr ss:[ebp + 8]
    add eax,10
    
    mov esp,ebp
    pop ebp
    ret
  MyProcB endp

  main PROC
    ; 第一種被調用者MyProcA平棧 3*4 = 12
    push 1
    push 2
    push 3
    call MyProcA
    invoke crt_printf,addr szFmt,eax
    
    ; 第二種方式:調用者平棧
    push 10
    call MyProcB
    add esp,4
    invoke crt_printf,addr szFmt,eax
    
    int 3
  main ENDP
END main

當然瞭如果讀者認為自己維護堆棧很繁瑣,則此時可以直接使用MASM彙編器提供的PROC定義過程,使用該偽指令彙編器會自行計算所需要使用的變數數量並自行在結尾處添加對應的平棧語句,這段代碼實現起來將變得非常容易理解。

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

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

.data
  szFmt BYTE '計算參數: %d ',0dh,0ah,0

.code
  my_proc PROC x:DWORD,y:DWORD,z:DWORD   ; 定義過程局部參數
    LOCAL @sum:DWORD               ; 定義局部變數存放總和
    
    mov eax,dword ptr ds:[x]
    mov ebx,dword ptr ds:[y]       ; 分別獲取到局部參數
    mov ecx,dword ptr ds:[z]
    
    add eax,ebx
    add eax,ecx                    ; 相加後放入eax
    mov @sum,eax
    ret
  my_proc endp

  main PROC
    LOCAL @ret_sum:DWORD
    push 10
    push 20
    push 30          ; 傳遞參數
    call my_proc
    mov @ret_sum,eax ; 獲取結果並列印
    
    invoke crt_printf,addr szFmt,@ret_sum
    
    int 3
  main ENDP
END main

這裡筆者還需要擴展一個偽指令LOCAL,LOCAL是一種彙編語言中的偽指令,用於定義存儲在堆棧上的局部變數。使用LOCAL指令定義的局部變數只在函數執行時存在,當函數返回後,該變數將被刪除。根據使用LOCAL指令時指定的記憶體空間大小,彙編器將為每個變數保留足夠的空間。

例如,下麵是一個使用LOCAL定義局部變數的示例:

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

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

.code
  main PROC
    ; 定義局部變數,自動壓棧/平棧
    LOCAL var_byte:BYTE,var_word:WORD,var_dword:DWORD
    LOCAL var_array[3]:DWORD
    
    ; 填充局部變數
    mov byte ptr ds:[var_byte],1
    mov word ptr ds:[var_word],2
    mov dword ptr ds:[var_dword],3
    
    ; 填充數組方式1
    lea esi,dword ptr ds:[var_array]
    mov dword ptr ds:[esi],10
    mov dword ptr ds:[esi + 4],20
    mov dword ptr ds:[esi + 8],30
    
    ; 填充數組方式2
    mov var_array[0],100
    mov var_array[1],200
    mov var_array[2],300
    
    invoke ExitProcess,0
  main ENDP
END main

在上述示例代碼中,main過程使用LOCAL指令定義了幾個局部變數,包括一個位元組類型的變數var_byte、一個字類型的變數var_word、一個雙字類型的變數var_dword和一個包含三個雙字元素的數組var_array

在代碼中,我們使用mov指令填充這些變數的值。對於位元組類型、字類型和雙字類型的變數,使用mov byte ptr ds:[var_byte], 1mov word ptr ds:[var_word], 2mov dword ptr ds:[var_dword], 3指令將相應的常數值存儲到變數中。在填充數組時,分別使用了兩種不同的方式。一種方式是使用lea指令將數組的地址載入到esi寄存器中,然後使用mov dword ptr ds:[esi],10等指令將相應的常數值存儲到數組中。另一種方式是直接訪問數組元素,如mov var_array[0], 100等指令。需要註意,由於數組元素在記憶體中是連續存儲的,因此可以使用[]操作符訪問數組元素。

在彙編中使用LOCAL偽指令來實現自動計算局部變數空間,以及最後的平棧操作,將會極大的提高開發效率。

10.4 USES/ENTER

USES是彙編語言中的偽指令,用於保存一組寄存器的狀態,以便函數調用過程中可以使用這些寄存器。使用USES時,程式可以保存一組需要保護的寄存器,彙編器將在程式入口處自動向堆棧壓入這些寄存器的值。讀者需註意,我們可以在需要保存寄存器的程式段中使用USES來保護寄存器,但不應在整個程式中重覆使用寄存器。

ENTER也是一種偽指令,用於創建函數調用過程中的堆棧幀。使用ENTER時,程式可以定義一個名為ENTER的指定大小的堆棧幀。該指令會將新的基準指針ebp 壓入堆棧同時將當前的基準指針ebp存儲到另一個寄存器ebx中,然後將堆棧指針esp減去指定大小的值,獲取新的基地址,並將新的基地址存儲到ebp 中。之後,程式可以在此幀上創建和訪問局部變數,並使用LEAVE指令將堆棧幀刪除,將ebp恢復為舊的值,同時將堆棧指針平衡。

在使用USES和ENTER指令時,需要瞭解這些指令在具體的平臺上的支持情況,以及它們適用的調用約定。通常情況下,在函數開頭,我們將使用ENTER創建堆棧幀,然後使用USES指定需要保護的寄存器。在函數末尾,我們使用LEAVE刪除堆棧幀。

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

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

.code
  ; USES 自動壓入 eax,ebx,ecx,edx
  my_proc PROC USES eax ebx ecx edx x:DWORD,y:DWORD
    enter 8,0          ; 自動保留8位元組堆棧空間
    add eax,ebx
    leave
  my_proc endp

  main PROC
    mov eax,10
    mov ebx,20
    call my_proc
    
    int 3
  main ENDP
END main

10.5 STRUCT/UNION

STRUCT和UNION是彙編語言中的數據類型,STRUCT是一種複合數據類型,它將多個不同類型的變數按順序放置在一起,並使用單個名稱來引用集合。使用STRUCT時,我們可以將不同類型的變數組合成一個結構體並定義其屬性,如結構體中包含的成員變數的數據類型、名稱和位置。

例如,下麵是一個使用STRUCT定義自定義類型的示例:

; 定義一個名為 MyStruct 的結構體,包含兩個成員變數。
MyStruct STRUCT
   Var1 DWORD ?
   Var2 WORD ?
MyStruct ENDS

在上述示例代碼中,我們使用STRUCT定義了一個名為MyStruct 的結構體,其中包含兩個成員變數Var1Var2。其中,Var1DWORD類型的數據類型,以問號?形式指定了其預設值,Var2WORD類型的數據類型。

另一個數據類型是UNION,它也是一種複合數據類型,用於將多個不同類型的變數疊加在同一記憶體位置上。使用UNION時,程式記憶體中的數據將只占用所有成員變數中最大的數據類型變數的大小。與結構體不同,聯合中的所有成員變數共用相同的記憶體位置。我們可以使用一種成員變數來引用記憶體位置,但在任何時候僅能有一種成員變數存儲在該記憶體位置中。

例如,下麵是一個使用UNION定義自定義類型的示例:

; 定義一個名為 MyUnion 的聯合,包含兩個成員變數。
MyUnion UNION
   Var1 DWORD ?
   Var2 WORD ?
MyUnion ENDS

在上述示例代碼中,我們使用UNION定義了一個名為MyUnion的聯合,其中包含兩個不同類型的成員變數Var1Var2,將它們相對應地置於聯合的同一記憶體位置上。

讀者在使用STRUCTUNION時,需要根據記憶體分佈和變數類型來正確訪問成員變數的值。在彙編語言中,結構體和聯合主要用於定義自定義數據類型、通信協議和系統數據結構等,如下一段代碼則是彙編語言中實現結構體賦值與取值的總結。

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

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

; 定義坐標結構
MyPoint Struct
  pos_x DWORD ?
  pos_y DWORD ?
  pos_z DWORD ?
MyPoint ends

; 定義人物結構
MyPerson Struct
  Fname db 20 dup(0)
  fAge  db 100
  fSex  db 20
MyPerson ends

.data
  ; 聲明結構: 使用 <>,{}符號均可
  PtrA MyPoint <10,20,30>
  PtrB MyPoint {100,200,300}
  
  ; 聲明結構: 使用MyPerson聲明結構
  UserA MyPerson <'lyshark',24,1>

.code
  main PROC
    ; 獲取結構中的數據
    lea esi,dword ptr ds:[PtrA]
    mov eax,(MyPoint ptr ds:[esi]).pos_x
    mov ebx,(MyPoint ptr ds:[esi]).pos_y
    mov ecx,(MyPoint ptr ds:[esi]).pos_z
    
    ; 向結構中寫入數據
    lea esi,dword ptr ds:[PtrB]
    mov (MyPoint ptr ds:[esi]).pos_x,10
    mov (MyPoint ptr ds:[esi]).pos_y,20
    mov (MyPoint ptr ds:[esi]).pos_z,30
    
    ; 直接獲取結構中的數據
    mov eax,dword ptr ds:[UserA.Fname]
    mov ebx,dword ptr ds:[UserA.fAge]
    int 3
  main ENDP
END main

接著我們來實現一個輸出結構體數組的功能,結構數組其實就是一維的空間,因此使用兩個比例因數即可實現定址操作,如下代碼我們先來實現一個簡單的功能,只遍歷第一層,結構數組外層的數據。

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

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

; 定義坐標結構
MyPoint Struct
  pos_x DWORD ?
  pos_y DWORD ?
  pos_z DWORD ?
MyPoint ends

; 定義迴圈結構
MyCount Struct
  count_x DWORD ?
  count_y DWORD ?
MyCount ends

.data
  ; 聲明結構: 使用 <>,{}符號均可
  PtrA  MyPoint <10,20,30>,<40,50,60>,<70,80,90>,<100,110,120>
  Count MyCount <0,0>
  szFmt BYTE '結構數據: %d',0dh,0ah,0

.code
  main PROC
    ; 獲取結構中的數據
    lea esi,dword ptr ds:[PtrA]
    mov eax,(MyPoint ptr ds:[esi]).pos_x          ; 獲取第一個結構X
    mov eax,(MyPoint ptr ds:[esi + 12]).pos_x     ; 獲取第二個結構X
    
    ; while 迴圈輸出結構的每個首元素元素
    mov (MyCount ptr ds:[Count]).count_x,0
  S1: cmp (MyCount ptr ds:[Count]).count_x,48        ; 12 * 4 = 48
    jge lop_end
    
    mov ecx,(MyCount ptr ds:[Count]).count_x
    mov eax,dword ptr ds:[PtrA + ecx]              ; 尋找首元素
    invoke crt_printf,addr szFmt,eax
    
    mov eax,(MyCount ptr ds:[Count]).count_x
    add eax,12                                     ; 每次遞增12
    mov (MyCount ptr ds:[Count]).count_x,eax
    jmp S1

  lop_end:
    int 3
  main ENDP
END main

接著我們遞增難度,通過每次遞增將兩者的偏移相加,獲得比例因數,通過因數嵌套雙層迴圈實現定址列印。

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

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

; 定義坐標結構
MyPoint Struct
  pos_x DWORD ?
  pos_y DWORD ?
  pos_z DWORD ?
MyPoint ends

; 定義迴圈結構
MyCount Struct
  count_x DWORD ?
  count_y DWORD ?
MyCount ends

.data
  ; 聲明結構: 使用 <>,{}符號均可
  PtrA  MyPoint <10,20,30>,<40,50,60>,<70,80,90>,<100,110,120>
  Count MyCount <0,0>
  szFmt BYTE '結構數據: %d',0dh,0ah,0

.code
  main PROC
    ; 獲取結構中的數據
    lea esi,dword ptr ds:[PtrA]
    mov eax,(MyPoint ptr ds:[esi]).pos_x          ; 獲取第一個結構X
    mov eax,(MyPoint ptr ds:[esi + 12]).pos_x     ; 獲取第二個結構X
    
    ; while 迴圈輸出結構的每個首元素元素
    mov (MyCount ptr ds:[Count]).count_x,0
  S1: cmp (MyCount ptr ds:[Count]).count_x,48        ; 12 * 4 = 48
    jge lop_end
    
    mov (MyCount ptr ds:[Count]).count_y,0
  S3: cmp (MyCount ptr ds:[Count]).count_y,12        ; 3 * 4 = 12
    jge S2
    
    mov eax,(MyCount ptr ds:[Count]).count_x
    add eax,(MyCount ptr ds:[Count]).count_y       ; 相加得到比例因數
    
    mov eax,dword ptr ds:[PtrA + eax]              ; 使用相對變址定址
    invoke crt_printf,addr szFmt,eax
    
    mov eax,(MyCount ptr ds:[Count]).count_y
    add eax,4                                      ; 每次遞增4
    mov (MyCount ptr ds:[Count]).count_y,eax
    jmp S3 
    
  S2: mov eax,(MyCount ptr ds:[Count]).count_x
    add eax,12                                     ; 每次遞增12
    mov (MyCount ptr ds:[Count]).count_x,eax
    jmp S1

  lop_end:
    int 3
  main ENDP
END main

結構體同樣支持內嵌的方式,如下Rect指針中內嵌兩個MyPoint分別指向左子域和右子域,這裡順便定義一個MyUnion聯合體把,其使用規範與結構體完全一致,只不過聯合體只能存儲一個數據.

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

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

; 定義坐標結構
MyPoint Struct
  pos_x DWORD ?
  pos_y DWORD ?
  pos_z DWORD ?
MyPoint ends

; 定義左右結構
Rect Struct
  Left MyPoint <>
  Right MyPoint <>
Rect ends

; 定義聯合體
MyUnion Union
  my_dword DWORD ?
  my_word WORD ?
  my_byte BYTE ?
MyUnion ends

.data
  PointA Rect <>
  PointB Rect {<10,20,30>,<100,200,300>}
  test_union MyUnion {1122h}
  szFmt BYTE '結構數據: %d',0dh,0ah,0
.code
  main PROC
    ; 嵌套結構的賦值
    mov dword ptr ds:[PointA.Left.pos_x],100
    mov dword ptr ds:[PointA.Left.pos_y],200
    mov dword ptr ds:[PointA.Right.pos_x],100
    mov dword ptr ds:[PointA.Right.pos_y],200
    
    ; 通過地址定位
    lea esi,dword ptr ds:[PointB]
    mov eax,dword ptr ds:[PointB]        ; 定位第一個MyPoint
    mov eax,dword ptr ds:[PointB + 12]   ; 定位第二個內嵌MyPoint

    ; 聯合體的使用
    mov eax,dword ptr ds:[test_union.my_dword]
    mov ax,word ptr ds:[test_union.my_word]
    mov al,byte ptr ds:[test_union.my_byte]
  main ENDP
END main

當然有了結構體這一成員的加入,我們同樣可以在彙編層面實現鏈表的定義與輸出,如下代碼所示,首先定義一個ListNode用於存儲鏈表結構的數據域與指針域,接著使用TotalNodeCount定義鏈表節點數量,最後使用REPEAT偽指令開闢ListNode對象的多個實例,其中的NodeData域包含一個1-15的數據,後面的($ + Counter * sizeof ListNode)則是指向下一個鏈表的頭指針,通過不斷遍歷則可輸出整個鏈表。

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

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

ListNode Struct
  NodeData DWORD ?
  NextPtr  DWORD ?
ListNode ends

TotalNodeCount = 15
Counter = 0

.data
  LinkList LABEL PTR ListNode
  REPEAT TotalNodeCount
    Counter = Counter + 1
    ListNode <Counter,($ + Counter * sizeof ListNode)>
  ENDM
  ListNode<0,0>                ; 標志著結構鏈表的結束

  szFmt BYTE '結構地址: %x 結構數據: %d',0dh,0ah,0
.code
  main PROC
    mov esi,offset LinkList
    
    ; 判斷下一個節點是否為<0,0>
  L1: mov eax,(ListNode PTR [esi]).NextPtr
    cmp eax,0
    je lop_end
    
    ; 顯示節點數據
    mov eax,(ListNode PTR [esi]).NodeData
    invoke crt_printf,addr szFmt,esi,eax
    
    ; 獲取到下一個節點的指針
    mov esi,(ListNode PTR [esi]).NextPtr
    jmp L1
    
  lop_end:
    int 3
    
  main ENDP
END main

本文作者: 王瑞
本文鏈接: https://www.lyshark.com/post/e43f6d19.html
版權聲明: 本博客所有文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 我們將介紹 Python 中的 gzip 解壓。我們還將介紹如何使用gzip解壓來解壓壓縮的內容。 ### Python中的Gzip解壓 在Python中為壓縮和解壓目的建立了許多庫,但我們將介紹Gzip 庫。它是一個流行的數據壓縮工具。 我們可以使用gzip ,通過對數據進行特殊格式的編碼來減少文 ...
  • 方法重載在Python中起著關鍵作用。方法有時接受零參數,有時接受一個或多個參數。 當我們以不同的方式調用同一個方法時,這就被稱為方法重載。Python不像其他語言那樣預設支持重載方法。 在Python中,兩個或多個方法不能有相同的名字,因為方法重載允許我們使同一個操作符具有不同的含義。讓我們詳細討 ...
  • # RUST 和 GO 如何管理它們的記憶體 ## Go 中的記憶體管理 Go 中的記憶體不會在緩存鍵被驅逐時立即釋放。 相反,垃圾收集器會經常運行以發現任何沒有引用的記憶體並釋放它。 換句話說,記憶體會一直掛起,直到垃圾收集器可以評估它是否真正不再使用,而不是在不再需要時立即釋放。 Go 必須付出的努力來找 ...
  • 安裝rabbitMQ的前提是安裝上erlang,所以從erlang安裝開始。 安裝erlang 1,先升級一下 $:sudo apt-get update 如果軟體源有問題 修改etc/apt/sources.list內為如下: # 國內源deb http://mirrors.aliyun.com/ ...
  • 給我一個CPU,給我一塊記憶體,我來執行一段代碼。 我要如何分配呢? ![](https://img2023.cnblogs.com/blog/3256961/202308/3256961-20230824111951962-1088592200.jpg) ` ` `new User();` 這裡有一 ...
  • 因為平時有多台設備要用,所以遠程桌面是我經常要使用的工具。 最近,正好看到一款不錯的遠程桌面軟體,馬上拿出來推薦給大家,如果有需要的可以看看。 ![](https://img2023.cnblogs.com/other/626506/202308/626506-20230824152205856-1 ...
  • 來源:blog.csdn.net/lvoelife/article/details/128092586 ## **1. 基本概念** 我們都使用過連接池,比如`C3P0,DBCP,hikari, Druid`,雖然HikariCP的速度稍快,但Druid能夠提供強大的監控和擴展功能,也是阿裡巴巴的開 ...
  • 在每個代碼範圍內使用足夠短和足夠長的名稱:例如迴圈計算器用一個字元就可以了,如i;條件和迴圈變數用一個單詞,方法名1-2個單詞,類名2-3個單詞,全局變數3-4個單片語成 為變數指定一些專門名稱,不要使用例如 "value", "equals", "data" 這樣的變數名 變數名要使用有意義的名稱 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...