久久精品精选,精品九九视频,www久久只有这里有精品,亚洲熟女乱色综合一区
    分享

    如何閱讀簡(jiǎn)單的匯編

     imnobody2001 2024-03-25 發(fā)布于江蘇

    現(xiàn)在高級(jí)語(yǔ)言”橫行“,閱讀匯編的能力似乎已經(jīng)不那么重要。但當(dāng)我們查看 core dump和有時(shí)候追求極致性能的時(shí)候,匯編還是要讀一讀。本文嘗試以閱讀簡(jiǎn)單匯編為出發(fā)點(diǎn),闡述一些基本技巧。

    本文是如何看懂程序Crash系列之一。

    跟高級(jí)語(yǔ)言相比,匯編晦澀難懂。但是它們是有規(guī)律可循的,并且有技巧可以幫忙。只要明白這些規(guī)律和技巧,閱讀基本的匯編很容易。下面讓我們來(lái)看看這些規(guī)律和技巧是什么。

    匯編是什么

    機(jī)器只能讀懂二進(jìn)制指令,而匯編是一組特定的字符,它們映射到二進(jìn)制指令,用于方便記憶和編寫(xiě)二進(jìn)制指令。比如move rax, rdx就是我們常見(jiàn)的匯編。匯編指令經(jīng)過(guò)匯編器(assembler)轉(zhuǎn)變成二進(jìn)制指令。

    圖片

    現(xiàn)在高級(jí)語(yǔ)言會(huì)編譯到匯編的有C/C++, Go, Rust。其他比如Java,C#,會(huì)編譯到虛擬機(jī)指令而不是直接的匯編。(虛擬機(jī)指令與匯編有許多共同的地方)

    什么時(shí)候會(huì)跟匯編打交道

    當(dāng)我們調(diào)試release版本的時(shí)候,debug symbols已經(jīng)被去掉了或者根本沒(méi)有。

    當(dāng)程序crash的時(shí)候,只有一個(gè)core dump,而且crash的地方不明所以。

    當(dāng)我們想要研究語(yǔ)言的一個(gè)高級(jí)特性性能如何。

    等等,這時(shí)候我們都要深入研究一下匯編,去讀懂匯編背后的邏輯。

    下面讓我們先看幾個(gè)簡(jiǎn)單的匯編例子。

    簡(jiǎn)單的匯編示例

    通過(guò)簡(jiǎn)單的匯編感受一下匯編。

    mov rbp, rsp 將寄存器rsp的值存儲(chǔ)到寄存器rbp中。

    mov DWORD PTR [rbp-4], 4將四個(gè)字節(jié)的4存儲(chǔ)到地址為rbp-4的棧上。(什么是4個(gè)字節(jié)的4?就是0x00000004,大小為四個(gè)字節(jié))

    sub rsp, 16將rsp的值減去16。

    上面的匯編格式Intel的語(yǔ)法。常見(jiàn)的匯編有兩種語(yǔ)法,一種是Intel,另一種是AT & T。Intel的格式是 opcode destination, source,類似于語(yǔ)法 int i = 4;而AT & T的格式是opcode source, destination,直觀理解為 move from source to destination。

    上面Intel的匯編,如果改寫(xiě)成AT & T,則為

    movq %rsp, %rbp

    movl $4, -4(%rbp)

    subq $16, %rsp

    AT & T的匯編另外一個(gè)特點(diǎn)是有前綴比如%,$。指令還有后綴q,l,等等。這些前綴后綴有特殊的意思,后文會(huì)講解。不同的格式側(cè)重點(diǎn)不太一樣,你可以選擇你喜歡的格式。

    如何一通百通匯編

    閱讀匯編的關(guān)鍵點(diǎn)是函數(shù)調(diào)用結(jié)構(gòu),數(shù)據(jù)傳輸方式,常見(jiàn)控制結(jié)構(gòu),具體指令功能。

    函數(shù)調(diào)用結(jié)構(gòu)

    讓我們以下面的簡(jiǎn)單代碼的為例,

    // 代碼1
    int square(int num) {
    return num * num;
    }
    int main() {
    int i = 4;
    int j = square(i);
    }

    看看它對(duì)應(yīng)的匯編

    //匯編1
    square(int):
    push rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-4], edi
    mov eax, DWORD PTR [rbp-4]
    imul eax, eax
    pop rbp
    ret
    main:
    push rbp
    mov rbp, rsp
    sub rsp, 16
    mov DWORD PTR [rbp-4], 4
    mov eax, DWORD PTR [rbp-4]
    mov edi, eax
    call square(int)
    mov DWORD PTR [rbp-8], eax
    mov eax, 0
    leave
    ret

    sqauremain前面的push rbpmov rbp, rsp又叫做函數(shù)的序言(prologue),幾乎每個(gè)函數(shù)一開(kāi)始都會(huì)有的指令。它和函數(shù)最后的pop rbpret(epilogue)起到維護(hù)函數(shù)的調(diào)用棧的作用。首先讓我們看看什么是函數(shù)的調(diào)用棧。

    程序都是一個(gè)函數(shù)(稱為caller)調(diào)用另外一個(gè)函數(shù)(稱為callee),這么嵌套下去(callee在調(diào)用其他函數(shù)的時(shí)候,自己就變成了caller,其他函數(shù)是它的callee)。

    為了在執(zhí)行完callee的時(shí)候可以跳轉(zhuǎn)回調(diào)用的地方(caller調(diào)用callee的下一個(gè)指令),程序會(huì)以棧的方式維護(hù)著函數(shù)的調(diào)用關(guān)系。具體是,每個(gè)函數(shù)都會(huì)對(duì)應(yīng)一個(gè)frame(棧幀),這個(gè)frame包含了用于恢復(fù)到caller的信息和當(dāng)前函數(shù)用于計(jì)算的數(shù)據(jù)(又稱局部變量)。

    見(jiàn)下圖,這些用于恢復(fù)的信息,包含返回地址,caller(previous frame)的rbp。(注意順序是先push 了返回地址,然后是rbp,如下圖灰色和綠色的框框。)函數(shù)調(diào)用的時(shí)候,callee的frame就會(huì)疊加在已有的frame上面,像一個(gè)盤(pán)子放在另外一個(gè)盤(pán)子上面,形成調(diào)用棧。藍(lán)色背景是callee的frame,橙色背景是caller的frame。

    圖片

    (注意:棧是向著低地址的方向生長(zhǎng))

    函數(shù)的調(diào)用棧,是理解匯編的第一道坎。第二道坎是函數(shù)的調(diào)用習(xí)慣(calling convention)也就是函數(shù)參數(shù)的存儲(chǔ)和傳遞方式。為了理解第二道坎,我們要先看看數(shù)據(jù)的傳遞。

    數(shù)據(jù)的傳遞

    函數(shù)在計(jì)算的時(shí)候,存儲(chǔ)數(shù)據(jù)的地方總共有三個(gè),寄存器,內(nèi)存和程序本身。寄存器的個(gè)數(shù)和名字取決于具體的計(jì)算機(jī)架構(gòu),本文以x86-64為例子。內(nèi)存分為棧空間和堆(heap)空間,靜態(tài)區(qū)。程序本身是指只讀的程序數(shù)據(jù)片段,比如int i = 4,這個(gè)4存儲(chǔ)于程序本身,在匯編里面又叫立即數(shù)(immediate number)。

    知道了數(shù)據(jù)的存儲(chǔ)地方,那么數(shù)據(jù)的傳遞就分為以下四個(gè)方面

    1. 從內(nèi)存到寄存器

    2. 從寄存器到內(nèi)存

    3. 從立即數(shù)到寄存器,

    4. 從立即數(shù)到內(nèi)存

    注意:數(shù)據(jù)不能從內(nèi)存直接傳遞到內(nèi)存。如果需要從內(nèi)存?zhèn)鬟f到內(nèi)存,要以寄存器為中介。(這些知識(shí),還是我當(dāng)年大學(xué)學(xué)的計(jì)算機(jī)組成原理里面的)

    數(shù)據(jù)是有大小的,比如一個(gè)word是兩個(gè)字節(jié),double words是四個(gè)字節(jié)。所以傳遞數(shù)據(jù)的時(shí)候,要知道傳遞的數(shù)據(jù)大小。Intel的匯編會(huì)在數(shù)據(jù)前面說(shuō)明數(shù)據(jù)大小,比如 mov DWORD PTR [rbp-4], 4,意思是將一個(gè)4字節(jié)的4存儲(chǔ)到 棧上(地址為rbp-4)。而AT & T是通過(guò)指令的后綴來(lái)說(shuō)明,同樣的指令為movl $4, -4(%rbp)。而存儲(chǔ)的地方,AT & T匯編是通過(guò)前綴來(lái)區(qū)別,比如%q前綴表示寄存器,$表示立即數(shù),()表示內(nèi)存。

    了解了數(shù)據(jù)的傳遞方式,那么讓我們看看函數(shù)的調(diào)用習(xí)慣。

    函數(shù)的調(diào)用習(xí)慣(calling convention)

    caller調(diào)用callee,要將參數(shù)(arguements)傳遞給callee。一個(gè)函數(shù)可以接收多個(gè)參數(shù),而caller與callee之間約定的每個(gè)參數(shù)的應(yīng)該怎么傳遞就是調(diào)用習(xí)慣。這樣子,callee就會(huì)到指定的位置獲取相應(yīng)的參數(shù)。

    比如一開(kāi)始的main調(diào)用square。參數(shù)i如何傳遞到square里面?通過(guò)閱讀上面的匯編,我們可以知道在main里面,4先存到棧上,然后存在edi里面,而sqaure函數(shù)直接從edi里面讀取4的值。這說(shuō)明了,參數(shù)4是通過(guò)寄存器edi傳給了calle (sqaure) 。可能有讀者會(huì)以為,從代碼看,參數(shù)不是直接就傳給了sqaure嗎。實(shí)際上,在匯編,這個(gè)變量i是不存在的,只有寄存器和內(nèi)存。我們需要約定好i的值存在哪里。

    下面讓我們看看這些約定:常見(jiàn)寄存器負(fù)責(zé)傳遞的參數(shù)以及一些作用

    圖片

    注意

    1. 淺藍(lán)色的是callee-owned。棕色背景的是caller-owned。callee-owned表明如果caller要使用這些寄存機(jī),那么它在調(diào)用callee前,要把這些寄存器保存好。caller-owned表明如果callee要使用這些寄存器,那么它就要保存好這些寄存器的值,并且返回到caller的時(shí)候要將這些值恢復(fù)。

    2. 一共有六個(gè)通用的寄存器用于傳遞參數(shù)。按順序傳遞需要通用寄存器傳遞的參數(shù),如果通用寄存器使用完了,那么就使用棧來(lái)傳遞(第一張圖)。詳細(xì)的規(guī)則記錄于Effective Debugging這本書(shū)里面。將另外用一篇文章來(lái)詳細(xì)說(shuō)明每個(gè)參數(shù)是如何傳遞的。

    3. 一共16個(gè)通用寄存機(jī),兩個(gè)特殊寄存器。前6個(gè)參數(shù)和返回值寄存器是callee-owned, callee可以自由地使用這些寄存器,覆蓋已有的值。如果%rax的值,caller想要保留,那么在調(diào)用函數(shù)之前,calleer需要賦值這個(gè)值到“安全”的地方。callee-owned的寄存器是callee理想的操作用具。相反,如果callee想要使用caller-owned的寄存器,那么它必須先保留原來(lái)的值,并且在退出調(diào)用時(shí)還原原來(lái)的值。caller-owned的寄存器通常用于caller需要在函數(shù)之間保留的局部狀態(tài)。

    有眼尖的讀者會(huì)發(fā)現(xiàn),匯編1里面,第一個(gè)參數(shù)是用edi來(lái)傳遞的,為什么這里是rdi?因?yàn)閞di是8字節(jié)的,4字節(jié)的時(shí)候?qū)?yīng)的就是edi。

    如果函數(shù)返回比較大的對(duì)象,那么第一個(gè)參數(shù)rdi會(huì)用來(lái)傳遞存儲(chǔ)這個(gè)對(duì)象的地址。這個(gè)地址是由caller分配的。有了這些基礎(chǔ),那么你就可以理解C++里面的copy elision了,可以挑戰(zhàn)一下Copy/move elision: C++ 17 vs C++ 11

    常見(jiàn)控制結(jié)構(gòu)

    這個(gè)對(duì)于入門(mén)的程序員很容易理解。控制結(jié)構(gòu)就是if, while循環(huán)等等。在匯編里面,它們都是基于判定語(yǔ)句,跳轉(zhuǎn)語(yǔ)句:做一個(gè)計(jì)算,檢查相應(yīng)的flag,然后根據(jù)flag的值確定要跳轉(zhuǎn)到哪里。比如下面的If語(yǔ)句

    // 代碼2
    if (j > 6) {
    std
    ::cout<<j*2;
    } else {
    std
    ::cout<<j*3;
    }

    對(duì)應(yīng)的匯編如下,cmpl $6, -8(%rbp) 根據(jù)對(duì)比結(jié)果,修改對(duì)應(yīng)的標(biāo)志位。下一行匯編jle .L4檢查對(duì)應(yīng)的標(biāo)志位,如果less and equal to 6,那么就跳轉(zhuǎn)到.L4,如果不是,就繼續(xù)執(zhí)行。

    # 匯編2
    cmpl $6, -8(%rbp)
    jle .L4
    movl -8(%rbp), %eax
    addl %eax, %eax
    movl %eax, %esi
    movl $_ZSt4cout, %edi
    call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
    jmp .L5
    .L4:
    movl -8(%rbp), %edx
    movl %edx, %eax
    addl %eax, %eax
    addl %edx, %eax
    movl %eax, %esi
    movl $_ZSt4cout, %edi
    call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
    .L5:
    movl $0, %eax
    leave
    ret

    指令

    對(duì)于指令,可以直接搜索得知具體的指令的作用,所以就不一一介紹了。講講一點(diǎn)小竅門(mén)。

    CMP destination, source;JBE .L3是指如果destination <= source則跳轉(zhuǎn)到.L3。

    技巧

    Compiler Explorer這個(gè)網(wǎng)站會(huì)顯示代碼對(duì)應(yīng)的匯編并且進(jìn)行了相應(yīng)的顏色匹配,非常方便查看匯編。而且鼠標(biāo)點(diǎn)擊相應(yīng)的匯編還會(huì)告訴提示,比如這個(gè)匯編是干什么的。所以我們可以借助這個(gè)網(wǎng)站來(lái)閱讀匯編。

    實(shí)際的例子

    查看crash的地方

    下面我將用gdb一步一步探索當(dāng)初在產(chǎn)品里的core dump。寫(xiě)下來(lái),也是為了以后我再次遇到相似的問(wèn)題可以有參考。

    某天,產(chǎn)品crash了,生成了core dump, 于是我們可以用命令gdb <exec> <core>來(lái)加載core dump。

    加載完core dump以后,截取crash附近的匯編如下

    0x00007ff967434cac <+188>: test %eax,%eax
    0x00007ff967434cae <+190>: js 0x7ff967434d53 <_ZN20ImportPackageUtility18hCreateUndoPackageERSt6vectorIhSaIhEE+355>
    0x00007ff967434cb4 <+196>: mov 0x0(%rbp),%rax
    0x00007ff967434cb8 <+200>: mov %rbp,%rdi
    0x00007ff967434cbb <+203>: callq *0x110(%rax)
    => 0x00007ff967434cc1 <+209>: mov (%rax),%rdx

    我們可以看到crash的地方是move (%rax), %rdx,那么我們查看寄存器rax的內(nèi)容,發(fā)現(xiàn)是0。加上這個(gè)core dump是segment fault,那么crash的理由大概是訪問(wèn)了空指針。

    接著,我們看看rax是怎么來(lái)的。上一條指令是call *0x110(%rax),猜想是訪問(wèn)虛函數(shù),想知道直覺(jué)是怎么來(lái)的,請(qǐng)看怎么理解C++虛函數(shù)?fat pointer in GO/Rust vs thin pointer in C++

    那么我們可以看看這個(gè)虛函數(shù)是什么。首先要知道現(xiàn)在rax的值。根據(jù)mov 0x0(%rbp), %rax,我們可以知道,rax等于rbp存儲(chǔ)的值,所以用下面的命令查看rbp存儲(chǔ)的內(nèi)容

    (gdb) x/gx $rbp
    0xc5089950: 0x00007ff95dc5f308

    接著,我們計(jì)算虛函數(shù)的地址為:p/x 0x110+$rax  = p/x 0x110 + 0x00007ff95dc5f308

    得到地址為0x00007ff95dc5f308,接著就可以查看這個(gè)地址存儲(chǔ)的虛函數(shù)是什么 (x/gx 0x7ff95dc5f418),發(fā)現(xiàn)是GetStream。所以我們可以知道GetStream返回了空指針。接下來(lái)我們就要查看產(chǎn)品代碼看看為什么會(huì)返回空指針。如果是正常的空指針,那么說(shuō)明crash的地方要檢查指針,如果不是正常的情況,那么我們就要修相應(yīng)的地方。

    全部的命令放到一塊就是

    (gdb) p/x $rbp
    $103 = 0xc5089950
    (gdb) x/gx 0xc5089950
    0xc5089950: 0x00007ff95dc5f308
    (gdb) p/x 0x00007ff95dc5f308+0x110
    $104 = 0x7ff95dc5f418
    (gdb) info symbol 0x7ff95dc5f418
    vtable for mpl + 288 in section .data.rel.ro of xx.so
    (gdb) x/gx 0x7ff95dc5f418
    0x7ff95dc5f418 <_ImplE+288>: 0x00007ff95da4dc60
    (gdb) info symbol 0x00007ff95da4dc60
    Impl::GetStream() in section .text of xx.so

    已知虛指針,打印虛函數(shù)表

    set $i = 0
    set $addr = <vtable address fromm info var classname>
    while $i < 10
    p $i
    p /a *((void**)($addr))
    set $addr = $addr + 8
    set $i = $i + 1
    end

    練習(xí)題

    題目一、請(qǐng)找出下面函數(shù)void g(s* p,int j)的參數(shù)傳遞



    p


    j


    g(s*, int):
    push rbp
    mov rbp, rsp
    sub rsp, 32
    mov QWORD PTR [rbp-24], rdi
    mov DWORD PTR [rbp-28], esi
    mov rax, QWORD PTR [rbp-24]
    mov eax, DWORD PTR [rax+16]
    mov esi, eax
    mov edi, OFFSET FLAT:_ZSt4cout
    call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
    mov eax, DWORD PTR [rbp-28]
    mov DWORD PTR [rbp-4], eax
    mov eax, DWORD PTR [rbp-4]
    mov esi, eax
    mov edi, OFFSET FLAT:_ZSt4cout
    call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
    nop
    leave
    ret
    main:
    push rbp
    mov rbp, rsp
    sub rsp, 32
    mov DWORD PTR [rbp-32], 7
    movsd xmm0, QWORD PTR .LC0[rip]
    movsd QWORD PTR [rbp-24], xmm0
    mov DWORD PTR [rbp-4], 4
    mov edx, DWORD PTR [rbp-4]
    lea rax, [rbp-32]
    mov esi, edx
    mov rdi, rax
    call g(s*, int)
    mov eax, 0
    leave
    ret
    __static_initialization_and_destruction_0(int, int):
    push rbp
    mov rbp, rsp
    sub rsp, 16
    mov DWORD PTR [rbp-4], edi
    mov DWORD PTR [rbp-8], esi
    cmp DWORD PTR [rbp-4], 1
    jne .L6
    cmp DWORD PTR [rbp-8], 65535
    jne .L6
    mov edi, OFFSET FLAT:_ZStL8__ioinit
    call std::ios_base::Init::Init() [complete object constructor]
    mov edx, OFFSET FLAT:__dso_handle
    mov esi, OFFSET FLAT:_ZStL8__ioinit
    mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
    call __cxa_atexit
    .L6:
    nop
    leave
    ret
    _GLOBAL__sub_I_g(s*, int):
    push rbp
    mov rbp, rsp
    mov esi, 65535
    mov edi, 1
    call __static_initialization_and_destruction_0(int, int)
    pop rbp
    ret
    .LC0:
    .long 1717986918
    .long 1074423398

    源碼是

    // Type your code here, or load an example.
    #include <iostream>
    class s {
    public:
    int i;
    double d;
    int j;
    };
    void g(s* p, int j) {
    std
    ::cout<< p->j;
    int k = j;
    std
    ::cout<<k;
    }
    int main() {

    s s1
    ;
    s1
    .i = 7;
    s1
    .d = 3.3;
    int i = 4;
    g(&s1, i);
    }

    題目二、找出下面Sum參數(shù)傳遞的寄存器或者棧

    class POD_STRUCT
    {
    public:
    short s;
    int a;
    double d;
    };
    class NONE_POD_STRUCT
    {
    virtual bool Verify() { return true; }

    public:
    short s;
    int a;
    double d;
    };
    double Sum(int i_int0,
    int i_int1,
    POD_STRUCT i_pod
    ,
    NONE_POD_STRUCT i_nonpod
    ,
    long *ip_long,
    float i_float,
    long i_long0,
    long i_long1)
    {
    double result = i_int0 + i_int1 + i_pod.a + i_pod.d + i_pod.s + i_nonpod.a + i_nonpod.d + i_nonpod.s + *ip_long + i_float + i_long0 + i_long1;
    int j = i_nonpod.a + 3;
    return result + j;
    }
    int main()
    {
    int a_int_0 = 0;
    int a_int_1 = 1;
    POD_STRUCT a_pod
    = {5, 1, 2.2};
    NONE_POD_STRUCT a_nonpod
    ;
    long a_long = 3;
    float a_float = 4.4;
    int j = a_nonpod.a + 4;
    double sum = Sum(a_int_0, a_int_1, a_pod, a_nonpod, &a_long, a_float, a_long, a_long);
    return 0;
    }

    答案是下圖

    圖片

    參考文獻(xiàn)

    《高效C/C++調(diào)試》清華大學(xué)出版社

    X86-64 Architecture Guide

    http://www.cs./~evans/cs216/guides/x86.html#calling

    https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-1.0.pdf

      本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
      轉(zhuǎn)藏 分享 獻(xiàn)花(0

      0條評(píng)論

      發(fā)表

      請(qǐng)遵守用戶 評(píng)論公約

      類似文章 更多

      主站蜘蛛池模板: 亚洲AV永久无码精品天堂动漫| 男人把女人桶爽30分钟| 性动态图AV无码专区| 久久人人爽人人人人片AV| 人妻精品动漫H无码中字| 亚洲欧美成人一区二区在线电影 | 国产精品一在线观看| 精品久久久久成人码免费动漫 | 国产精品午夜av福利| 成 人 色 网 站免费观看| 成人片黄网站色大片免费观看软件| 亚洲欧美偷国产日韩| 亚洲人成伊人成综合网久久久| 精品中文字幕人妻一二| 日韩A人毛片精品无人区乱码| 日韩深夜视频在线观看| 久久久久久伊人高潮影院| 国产一区日韩二区欧美三区| 18禁超污无遮挡无码网址| 色综合久久久无码中文字幕 | 国产在线不卡精品网站| 99国产成人综合久久精品 | 免费人成视频网站在线18| 播放灌醉水嫩大学生国内精品| 色狠狠色噜噜AV一区| 中文文字幕文字幕亚洲色| 东京热TOKYO综合久久精品| 国产一区二区日韩经典| 色婷婷综合久久久久中文字幕| 激情五月开心婷婷深爱| 亚洲精品爆乳一区二区H| 欧美成人VA免费大片视频| 久久精品国产亚洲av麻豆长发| 中文字幕无码免费久久99| 欧美激情一区二区三区成人| 久久久久高潮综合影院| 亚洲欧美高清在线精品一区二区| 狠狠人妻久久久久久综合| 成年女人碰碰碰视频播放| 国产中文字幕在线精品| 亚洲综合激情另类小说区|