Điều đầu tiên và quan trọng nhất khi đọc về C hay assembly là: không được sợ. Code có thể lạ hay dài, nhưng không hề khó, mà thường đơn giản.

  • không yêu cầu biết C
  • không yêu cầu biết assembly
  • biết code đơn giản 1 ngôn ngữ bất kỳ

đọc xong là biết.

Bài viết liên quan hơi ngược lại với bài viết hello word bằng x86-64 assembly, viết thì khó hơn đọc, bởi viết cần nghĩ đủ thứ, lên kế hoạch viết gì tiếp, đặt tên ra sao, còn đọc thì chỉ cần đi theo.

PS: nên đọc trên máy tính hay tablet để có format chuẩn.

Cài đặt gcc gdb

Trên Ubuntu

sudo apt update && sudo apt install -y gcc gdb

Viết Hello World bằng C

Hơn "hello world" 1 chút, sẽ viết 1 function nhận vào 8 đầu vào và tính tổng.

#include <stdio.h>

int sum(int a, int b, int c, int d, int e, int f, int g, int h) {
    int s = a + b + c + d + e + f + g + h;
    return s;
}

int main() {
    puts("Hello world!");
    int s = sum(1,2,3,4,5,6,7,8);
    return s*2;
}

Dòng include như import trong Python để C có thể gọi function puts. Còn lại, code trên tương đương code Python3 sau:

def sum(a: int, b: int, c: int, d: int, e: int, f: int, g: int, h: int) -> int:
    s: int = a + b + c + d + e + f + g + h
    return s

def main() -> int:
    print("Hello world!")
    s: int = sum(1,2,3,4,5,6,7,8)
    return s*2

Dùng gcc Compile & link code thành file binary

$ gcc --help
Usage: gcc [options] file...
...
  --help={common

$ gcc --help=common | grep debug
  --debug                     Same as -g.
$ gcc -g hello.c
$ file a.out
a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=80dbc7d0bdab643730e858ec44e97fcd92dacb7b, for GNU/Linux 4.4.0, with debug_info, not stripped

Chạy code

$ ./a.out
Hello world!
$ echo $?
72

Vì main return (1+2+3+4+5+6+7+8)*2 ta được kết quả 72.

Các lệnh gdb cơ bản

  • info hiển thị các thông tin, gõ info sẽ hiện các lệnh con như info functions
File hello.c:
8:  int main();
3:  int sum(int, int, int, int, int, int, int, int);

Non-debugging symbols:
0x0000000000001000  _init
0x0000000000001030  puts@plt
0x0000000000001040  _start
0x00000000000011dc  _fini
  • Code assembly có 2 syntax, gdb mặc định là AT&T att , hoặc phổ biến (cả trên Windows) là intel. Lệnh set disassembly-flavor intel để chọn intel syntax.
(gdb) set disassembly-flavor intel
  • disas hay disassemble in ra code assembly của function tương ứng. Gõ help disas để hiện thêm chi tiết các option
(gdb) help disas
With a /s modifier, source lines are included (if available).
In this mode, the output is displayed in PC address order, and
file names and contents for all relevant source files are displayed.
...

help disas /s main để hiện code C kèm asm:

(gdb) disas /s main
Dump of assembler code for function main:
hello.c:
8   int main() {
   0x000000000000118f <+0>: push   rbp
   0x0000000000001190 <+1>: mov    rbp,rsp
   0x0000000000001193 <+4>: sub    rsp,0x10

9       puts("Hello world!");
   0x0000000000001197 <+8>: lea    rax,[rip+0xe66]        # 0x2004
   0x000000000000119e <+15>:    mov    rdi,rax
   0x00000000000011a1 <+18>:    call   0x1030 <puts@plt>

10      int s = sum(1,2,3,4,5,6,7,8);
   0x00000000000011a6 <+23>:    push   0x8
   0x00000000000011a8 <+25>:    push   0x7
   0x00000000000011aa <+27>:    mov    r9d,0x6
   0x00000000000011b0 <+33>:    mov    r8d,0x5
   0x00000000000011b6 <+39>:    mov    ecx,0x4
   0x00000000000011bb <+44>:    mov    edx,0x3
   0x00000000000011c0 <+49>:    mov    esi,0x2
   0x00000000000011c5 <+54>:    mov    edi,0x1
   0x00000000000011ca <+59>:    call   0x1139 <sum>
   0x00000000000011cf <+64>:    add    rsp,0x10
   0x00000000000011d3 <+68>:    mov    DWORD PTR [rbp-0x4],eax

11      return s;
   0x00000000000011d6 <+71>:    mov    eax,DWORD PTR [rbp-0x4]

12  }
   0x00000000000011d9 <+74>:    leave
   0x00000000000011da <+75>:    ret
End of assembler dump.

Giải thích code assembly trong hello world

Code assembly tuy dài nhưng đơn giản, chỉ dùng vài instruction (câu lệnh) như:

  • mov
  • call
  • lea
  • push
  • add
  • sub
  • leave
  • ret

Các register trong assembly x86-64

asm x86-64 có 16 register (thanh ghi) thường dùng sau

  • rbp: stack-frame base pointer
  • rsp: (top of) stack pointer
  • rax: accumulator - thường chứa kết quả của các phép tính
  • rbx
  • rcx
  • rdx
  • rdi
  • rsi

và các register chỉ có trong x86-64 (64 bits), không có trong x86 (32 bits):

  • r8d
  • r9d
  • ...
  • r15d

chúng như các "biến" với tên cố định trên CPU để chứa các giá trị.

  • rip: instruction pointer là register đặc biệt, trỏ tới instruction tiếp theo được chạy.

Các register đều có kích thước 64 bits, ở dạng 32 bits, tên của chúng thay chữ r bằng chữ e: eip, esp, ebp, eax, ebx, ecx, edx, edi, esi.

rbp - base pointer register và stack

8   int main() {
   0x000000000000118f <+0>: push   rbp
   0x0000000000001190 <+1>: mov    rbp,rsp
   0x0000000000001193 <+4>: sub    rsp,0x10

Khi vào 1 function, dòng đầu tiên luôn là push rbp để lưu giá trị hiện tại của rbp vào stack. Stack là 1 vùng bộ nhớ liên tục, thường được chia thành các frames. Mỗi function khi chạy có 1 stack-frame để lưu các thông tin của function đang chạy (biến local, parameter để gọi function khác ...). Stack giống như chồng sách, xếp vào trước sẽ ở dưới, lấy ra sau cùng. rbp khi ở địa chỉ 0x00118f chứa "base pointer" của function _start, function gọi main (lập trình viên C phải viết chương trình chạy từ main_start chỉ gọi main). rbp sẽ được dùng trong main để lưu "base pointer" của main, nên sẽ đè mất rbp của _start, do vậy phải push rbp vào stack. Tương ứng với nó, cuối function sẽ có instruction leave, thực chất là pop stack để lấy ra rbp đã lưu.

12  }
   0x00000000000011db <+76>:    leave
   0x00000000000011dc <+77>:    ret

Cú pháp asm instruction

Hầu hết ở 1 trong các dạng

instruction
instruction register
instruction register value
instruction register [memory]

Hai dòng tiếp theo khi bắt đầu main

   0x0000000000001190 <+1>: mov    rbp,rsp
   0x0000000000001193 <+4>: sub    rsp,0x10

mov thực hiện lấy giá trị của rsp ghi vào rbp, hay dễ hiểu hơn, như viết rbp = rsp trong C, Python. sub trừ địa chỉ rsp đi 0x10 hay 16 đơn vị.

Hiển thị hello world!

9       puts("Hello world!");
   0x0000000000001197 <+8>: lea    rax,[rip+0xe66]        # 0x2004
   0x000000000000119e <+15>:    mov    rdi,rax
   0x00000000000011a1 <+18>:    call   0x1030 <puts@plt>

Gồm 3 bước:

  • tính vị tri của string "Hello world!" trong file binary. lea Load effective address tính địa chỉ rồi gán cho rax: rax = rip + 0xe66, ở đây tính địa chỉ, không đọc nội dung.
  • gán rdi cho giá trị này rdi = rax
  • call gọi function ở vị trí 0x1030, tức function puts với 1 argument được chứa trong rdi. Và bắt buộc phải là rdi, lý do bởi "call convention".

Call convention

Định nghĩa trong System V AMD64 ABI, mục 3.2.3 Parameter passing:

  1. If the class is INTEGER, the next available register of the sequence %rdi, %rsi, %rdx, %rcx, %r8 and %r9 is used
  • Lần lượt, rsi, rdi, rdx, rcx, r8, r9 chứa các argument 1-6
  • argument thứ 7 trở đi được push vào stack. Đây là lý do các function C thường không dùng hơn 6 argument để tối ưu tốc độ, các register luôn nhanh hơn push vào stack.
  • thứ tự các câu lệnh gán argument thực hiện từ phải qua trái (từ 8 đến 1): push 8, push 7, r9d=6, r8d=5, ecx=4, edx=3, esi=2, edi=1. GCC đã thực hiện tối ưu, nó phát hiện giá trị đủ nhỏ để chứa trong 32bits nên dùng ecx chứ không dùng rcx.
  • gọi call 0x1139, 0x1139 là địa chỉ của function sum.
10      int s = sum(1,2,3,4,5,6,7,8);
   0x00000000000011a6 <+23>:    push   0x8
   0x00000000000011a8 <+25>:    push   0x7
   0x00000000000011aa <+27>:    mov    r9d,0x6
   0x00000000000011b0 <+33>:    mov    r8d,0x5
   0x00000000000011b6 <+39>:    mov    ecx,0x4
   0x00000000000011bb <+44>:    mov    edx,0x3
   0x00000000000011c0 <+49>:    mov    esi,0x2
   0x00000000000011c5 <+54>:    mov    edi,0x1
   0x00000000000011ca <+59>:    call   0x1139 <sum>
   0x00000000000011cf <+64>:    add    rsp,0x10
   0x00000000000011d3 <+68>:    mov    DWORD PTR [rbp-0x4],eax
  • kết quả của function sum tự được chứa trong register eax. mov gán giá trị return của sum vào địa chỉ rbp-0x4.
11      return s*2;
   0x00000000000011d6 <+71>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000000011d9 <+74>:    add    eax,eax

giá trị được gán vào eax rồi thực hiện *2 bằng cách cộng eax với eax qua add eax,eax. Kết quả của phép tính này tự được chứa trong eax, là giá trị main trả về. Chú ý, ở đây do GCC thực hiện tối ưu, nên thay phép nhân bằng phép cộng 2 số.

Các kiểu dữ liệu trong assembly - data type

Chapter 4.1 vol 1 Intel SDM viết:

A byte is eight bits, a word is 2 bytes (16 bits), a doubleword is 4 bytes (32 bits), a quadword is 8 bytes (64 bits), and a double quadword is 16 bytes (128 bits).

doubleword trong asm được ký hiệu là DWORD, quadword ký hiệu là QWORD.

Đọc assembly function sum

(gdb) disas /s sum
Dump of assembler code for function sum:
hello.c:
3   int sum(int a, int b, int c, int d, int e, int f, int g, int h) {
   0x0000000000001139 <+0>: push   rbp
   0x000000000000113a <+1>: mov    rbp,rsp
   0x000000000000113d <+4>: mov    DWORD PTR [rbp-0x14],edi
   0x0000000000001140 <+7>: mov    DWORD PTR [rbp-0x18],esi
   0x0000000000001143 <+10>:    mov    DWORD PTR [rbp-0x1c],edx
   0x0000000000001146 <+13>:    mov    DWORD PTR [rbp-0x20],ecx
   0x0000000000001149 <+16>:    mov    DWORD PTR [rbp-0x24],r8d
   0x000000000000114d <+20>:    mov    DWORD PTR [rbp-0x28],r9d

gán lần lượt trái qua phải (1-6) các register cho các địa chỉ dưới (nhỏ hơn) rbp. Chú ý chỉ là 6, 2 phần tử 7 8 trong stack chưa được xử lý.

4       int s = a + b + c + d + e + f + g + h;
   0x0000000000001151 <+24>:    mov    edx,DWORD PTR [rbp-0x14]
   0x0000000000001154 <+27>:    mov    eax,DWORD PTR [rbp-0x18]
   0x0000000000001157 <+30>:    add    edx,eax
   0x0000000000001159 <+32>:    mov    eax,DWORD PTR [rbp-0x1c]
   0x000000000000115c <+35>:    add    edx,eax
   0x000000000000115e <+37>:    mov    eax,DWORD PTR [rbp-0x20]
   0x0000000000001161 <+40>:    add    edx,eax
   0x0000000000001163 <+42>:    mov    eax,DWORD PTR [rbp-0x24]
   0x0000000000001166 <+45>:    add    edx,eax
   0x0000000000001168 <+47>:    mov    eax,DWORD PTR [rbp-0x28]
   0x000000000000116b <+50>:    add    edx,eax
   0x000000000000116d <+52>:    mov    eax,DWORD PTR [rbp+0x10]
   0x0000000000001170 <+55>:    add    edx,eax
   0x0000000000001172 <+57>:    mov    eax,DWORD PTR [rbp+0x18]
   0x0000000000001175 <+60>:    add    eax,edx
   0x0000000000001177 <+62>:    mov    DWORD PTR [rbp-0x4],eax

5       return s;
   0x000000000000117a <+65>:    mov    eax,DWORD PTR [rbp-0x4]

6   }
   0x000000000000117d <+68>:    pop    rbp
   0x000000000000117e <+69>:    ret

Thực hiện cộng rồi trả về kết quả. Truy cập argument 7 8 qua địa chỉ trên (lớn hơn) rbp: rbp+0x10 và rbp+0x18.

Thực hiện trên

$ gcc --version
gcc (GCC) 13.1.1 20230429
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gdb --version
GNU gdb (GDB) 13.1
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$ uname -a
Linux zb 6.1.38-1-MANJARO #1 SMP PREEMPT_DYNAMIC Wed Jul  5 23:49:30 UTC 2023 x86_64 GNU/Linux

Tham khảo

Kết luận

Assembly đơn giản, dễ đọc, khó viết.

Phần sau sẽ chạy qua từng dòng code với gdb để xem khi chạy các giá trị được lưu trữ và tính toán thế nào.

Hết.

HVN at http://pymi.vn and https://www.familug.org.

Ủng hộ tác giả 🍺



Published

Category

frontpage

Tags

Contact