寫在前面的話
高級語言有Java golang C等,通過系統調用訪問系統的資源,那底層的匯編代碼是如何運行的,此文通過匯編語言簡單的說明系統調用。
環境准備安裝nasm
osx系統通過brew安裝
brew install nasm
CentOS7環境下源碼安裝
下載匯編編譯器nasm:https://www.nasm.us/
wget https://www.nasm.us/pub/nasm/releasebuilds/2.15.05/nasm-2.15.05.tar.gz tar -xvf nasm-2.15.05.tar.gz && cd nasm-2.15.05.tar.gz && ./configure && make && make install
Unbuntu環境下安裝
sudo apt-get install nasm
匯編說明
一個匯編的簡單例子 hello.asm
section .data msg: db "hello world", 0x0a len: equ $-msg SYS_WRITE equ 1 STD_OUT equ 1 SYS_EXIT equ 60 section .text global _start _start: mov rax, SYS_WRITE mov rdi,STD_OUT mov rsi,msg mov rdx,len syscall jmp exit exit: mov rax,SYS_EXIT mov rdi,0 syscall
編譯如下匯編文件
hello: nasm -f elf64 -o hello.o hello.asm ld -o hello -e _start hello.o clean: rm hello hello.o
nasm支持的輸出文件格式包括 linux的elf64 elf32以及macox的macho32 mach64等
使用C代碼解析
#include <stdio.h> const char *msg= "hello world\n"; const int len = 12; int main() { write(1, msg, len); exit(0); return 0; }
將如下C代碼編譯成匯編
gcc -S hello.c產生hello.s匯編文件內容如下
.file "hello.c" .globl msg .section .rodata .LC0: .string "hello world\n" .data .align 8 .type msg, @object .size msg, 8 msg: .quad .LC0 .globl len .section .rodata .align 4 .type len, @object .size len, 4 len: .long 12 .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $12, %edx movq msg(%rip), %rax movq %rax, %rsi movl $1, %edi movl $0, %eax call write movl $0, %edi call exit .cfi_endproc
對比如上兩個匯編,基本一致。
數據段 .data
數據段用於定義常量,在運行時不可改變,定義語法如下
section .data msg: db "hello world", 0x0a len: equ $-msg
解析:
定義數據段: section .data
聲明一個字符串,以換行結尾:msg: db "hello world", 0x0a
對應的C代碼的
const char *msg= "hello world\n";
db的含義是定義字節 byte,每個字符是一個字節。
另外還有兩個字節的dw,以及其他的
dx := DB | DW | DD | DQ | DT | DO | DY | DZ
type := BYTE | WORD | DWORD | QWORD | TWORD | OWORD | YWORD | ZWORD
聲明一個長度常量,取值字符串的長度:len: equ $-msg
equ的含義是定義一個常量的符號,取值是一個常量。
對應的C代碼
const int len = 12;
代碼段 .text
代碼段用於代碼,代碼段需要以global _start開頭,告訴系統這是代碼的入口,定義語法如下:
section .text
global _start
_start:
解析:
定義代碼段:section .text
定義代碼的全局入口標簽:global _start
代碼標簽開始:_start:
從CPU運行的角度分析代碼段
CPU簡潔執行步驟是 加載指令,解碼指令,運行指令
CPU在時鍾驅動下,從內存加載,解碼和運行指令順序如下所示:
控制台標准輸出字符串的匯編解析
mov rax,1 mov rdi,1 mov rsi,msg mov rdx,len syscall
對應C代碼
write(1, hello, 12);
解析:
mov rax,1 表示將__NR_write的系統調用號賦值給寄存器RAX,對應write系統調用函數,#define __NR_write 1
mov rdi,1 表示給系統調用write傳遞第一個參數,參數值等於1,表示控制台標准輸出stdout
mov rsi, msg 表示給系統調用write傳遞第二個參數,參數值等於msg字符串指針,即"hello world\n"
mov rdx, len 表示給系統調用write傳遞第三個參數,參數值等於msg字符串的長度,即12
syscall 表示執行系統調用write
本文使用osx和Centos7系統實驗。
退出程序的匯編
mov rax,60 mov rdi,0 syscall
對應C代碼
exit(0);
解析:
mov rax,60 表示將__NR_exit的系統調用號賦值給寄存器RAX,對應exit系統調用函數,#define __NR_exit 60
mov rdi,0 表示給系統調用exit傳遞第一個參數,參數值等於0
syscall 表示執行系統調用exit
在linux系統上可以查看/usr/include/asm/unistd_64.h獲取常用的系統調用函數的系統調用號
#define __NR_read 0 #define __NR_write 1 #define __NR_open 2 #define __NR_close 3 // ... #define __NR_exit 60 // ... #define __NR_pkey_free 331
如上就將簡單的helloworld的匯編解析完畢,那么為什么要使用到rax rdi rsi rdx這些寄存器呢,
原因是CPU規定64位系統函數調用的參數傳遞使用的寄存器如下
對應的macos的代碼如下,區別是不同系統的系統調用號不同
hello.asm
section .data msg: db "hello world", 0x0a len: equ $-msg SYS_WRITE equ 0x2000004 STD_OUT equ 1 SYS_EXIT equ 0x2000001 section .text global _start _start: mov rax, SYS_WRITE mov rdi,STD_OUT mov rsi,msg mov rdx,len syscall jmp exit exit: mov rax,SYS_EXIT mov rdi,0 syscall
編譯的Makefile
hello: nasm -f macho64 -o hello.o hello.asm ld -o hello -e _start hello.o -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib -lSystem clean: rm hello hello.o
在macos系統上生成二進制文件需要鏈接 -lSystem庫才可以執行。
更多的匯編代碼可以學習nasm的匯編文檔說明:
https://www.nasm.us/pub/nasm/releasebuilds/2.15.05
參考材料:
https://0xax.blogspot.com/2014/08/say-hello-to-x64-assembly-part-1.html
https://github.com/0xAX/asm
祝玩的開心~
done.