Intel x86 Assembly Language & MicroarchitectureIntel x86 어셈블리 언어 및 마이크로 아키텍처 시작하기


비고

이 절에서는 x86이 무엇인지, 왜 개발자가 그것을 사용하고 싶어하는지에 대한 개요를 제공합니다.

또한 x86 내의 모든 큰 주제를 언급하고 관련 주제에 링크해야합니다. x86 용 설명서는 새로운 기능이므로 해당 관련 항목의 초기 버전을 만들어야 할 수도 있습니다.

x86 어셈블리 언어

x86 어셈블리 언어 제품군은 원래의 Intel 8086 아키텍처에서 수십 년간 진보했습니다. 사용 된 어셈블러를 기반으로하는 여러 가지 방언이 있다는 것 외에도 추가 프로세서 지침, 레지스터 및 기타 기능이 1980 년에 사용 된 16 비트 어셈블리와 역 호환을 유지하면서 추가되었습니다.

x86 어셈블리 작업의 첫 번째 단계는 목표가 무엇인지 결정하는 것입니다. 예를 들어 운영 체제에서 코드를 작성하려는 경우 독립 실행 형 어셈블러를 사용할지 또는 C와 같은 고급 언어의 기본 제공 인라인 어셈블리 기능을 사용할지 여부를 추가로 결정해야합니다. 운영 체제가없는 "베어 메탈"에 코드를 작성하려면 원하는 어셈블러를 설치하고 플래시 메모리, 부트 가능한 이미지 또는 메모리로로드 될 수있는 바이너리 코드를 작성하는 방법을 이해하기 만하면됩니다. 실행을 시작할 적절한 위치.

많은 플랫폼에서 잘 지원되는 매우 유명한 어셈블러는 http://nasm.us/ 에서 얻을 수있는 NASM (Netwide Assembler)입니다. NASM 사이트에서 플랫폼에 맞는 최신 릴리스 빌드를 다운로드 할 수 있습니다.

Windows

Windows에서 32 비트 및 64 비트 버전의 NASM을 사용할 수 있습니다. NASM은 Windows 호스트에서 어셈블러를 자동으로 설치하는 데 사용할 수있는 편리한 설치 프로그램과 함께 제공됩니다.

리눅스

NASM이 이미 Linux 버전에 설치되어있을 수 있습니다. 확인하려면 다음을 실행하십시오.

nasm -v
 

명령을 찾을 수 없으면 설치를 수행해야합니다. 최첨단 NASM 기능을 필요로하는 것을하지 않는 한 가장 좋은 방법은 Linux 배포판에 내장 된 패키지 관리 도구를 사용하여 NASM을 설치하는 것입니다. 예를 들어 Ubuntu와 같은 데비안에서 파생 된 시스템에서는 명령 프롬프트에서 다음을 실행합니다.

sudo apt-get install nasm
 

RPM 기반 시스템의 경우 다음을 시도해보십시오.

sudo yum install nasm
 

맥 OS X

최신 버전의 OS X (Yosemite 및 El Capitan 포함)에는 이전 버전의 NASM이 사전 설치되어 있습니다. 예를 들어 El Capitan의 버전은 0.98.40입니다. 이것은 거의 모든 정상적인 목적으로 작동하지만 실제로는 꽤 오래된 것입니다. 이 글을 쓰면서 NASM 버전 2.11이 출시되었고 2.12에는 출시 후보가 많이있었습니다.

위의 링크에서 NASM 소스 코드를 얻을 수 있지만 소스에서 특정 설치가 필요하지 않으면 OS X 릴리스 디렉토리에서 바이너리 패키지를 다운로드하고 압축을 해제하는 것이 훨씬 간단합니다.

일단 압축을 풀면 NASM의 시스템 설치 버전을 덮어 쓰지 않는 것이 좋습니다. 대신 / usr / local에 설치할 수 있습니다.

 $ sudo su
 <user's password entered to become root>
 # cd /usr/local/bin
 # cp <path/to/unzipped/nasm/files/nasm> ./
 # exit
 

이 시점에서 NASM은 /usr/local/bin 에 있지만 경로에 없습니다. 이제 프로필 끝에 다음 줄을 추가해야합니다.

 $ echo 'export PATH=/usr/local/bin:$PATH' >> ~/.bash_profile
 

이렇게하면 경로에 /usr/local/bin 이 추가됩니다. 명령 프롬프트에서 nasm -v 를 실행하면 적절한 최신 버전이 표시됩니다.

x86 Linux Hello World 예제

이것은 libc 함수 호출없이 시스템 호출을 직접 사용하여 32 비트 x86 Linux 용 NASM 어셈블리의 기본 Hello World 프로그램입니다. 받아 들여야 할 것이 많지만 시간이 지나면 이해할 수있게 될 것입니다. 세미콜론 ( ; )으로 시작하는 줄은 주석입니다.

저급 Unix 시스템 프로그래밍에 대해 아직 모르는 경우에는 asm으로 함수를 작성하고 C 또는 C ++ 프로그램에서 함수를 호출하는 것이 좋습니다. 그렇다면 POSIX 시스템 호출 API와 ABI를 사용하지 않고도 레지스터와 메모리를 처리하는 방법을 배우는 것에 대해 걱정할 수 있습니다.


이렇게하면 write(2) _exit(2) (stdio 버퍼를 플러시하는 exit(3) libc 래퍼가 아닌 write(2) 등 두 가지 시스템 호출이 생성됩니다. (기술적으로, _exit() 는 sys_exit가 아닌 sys_exit_group을 호출하지만 멀티 스레드 프로세스 에서만 중요합니다 .) 일반적으로 시스템 호출에 대한 문서는 syscalls(2) 를 참조하십시오. 직접 호출하는 것과 libc를 사용하는 것의 차이점 랩퍼 기능.

요약하면 시스템 호출은 arg를 적절한 레지스터에두고 시스템 호출 번호를 eax 로 지정한 다음 int 0x80 명령을 실행하여 수행됩니다. 또한 Assembly에서 시스템 호출의 반환 값은 무엇입니까? asm syscall 인터페이스가 대부분 C 구문으로 문서화되는 방법에 대한 자세한 설명은

32 비트 ABI의 syscall 호출 번호는 /usr/include/i386-linux-gnu/asm/unistd_32.h ( /usr/include/x86_64-linux-gnu/asm/unistd_32.h 내용과 동일).

#include <sys/syscall.h> 는 궁극적으로 올바른 파일을 포함하므로 echo '#include <sys/syscall.h>' | gcc -E - -dM | less 매크로 def를 볼 수 있습니다 ( C 헤더의 asm에 대한 상수 찾기에 대한 자세한 내용이 답변 참조)


section .text             ; Executable code goes in the .text section
global _start             ; The linker looks for this symbol to set the process entry point, so execution start here
;;;a name followed by a colon defines a symbol.  The global _start directive modifies it so it's a global symbol, not just one that we can CALL or JMP to from inside the asm.
;;; note that _start isn't really a "function".  You can't return from it, and the kernel passes argc, argv, and env differently than main() would expect.
 _start:
    ;;; write(1, msg, len);
    ; Start by moving the arguments into registers, where the kernel will look for them
    mov     edx,len       ; 3rd arg goes in edx: buffer length
    mov     ecx,msg       ; 2nd arg goes in ecx: pointer to the buffer
    ;Set output to stdout (goes to your terminal, or wherever you redirect or pipe)
    mov     ebx,1         ; 1st arg goes in ebx: Unix file descriptor. 1 = stdout, which is normally connected to the terminal.

    mov     eax,4         ; system call number (from SYS_write / __NR_write from unistd_32.h).
    int     0x80          ; generate an interrupt, activating the kernel's system-call handling code.  64-bit code uses a different instruction, different registers, and different call numbers.
    ;; eax = return value, all other registers unchanged.

    ;;;Second, exit the process.  There's nothing to return to, so we can't use a ret instruction (like we could if this was main() or any function with a caller)
    ;;; If we don't exit, execution continues into whatever bytes are next in the memory page,
    ;;; typically leading to a segmentation fault because the padding 00 00 decodes to  add [eax],al.

    ;;; _exit(0);
    xor     ebx,ebx       ; first arg = exit status = 0.  (will be truncated to 8 bits).  Zeroing registers is a special case on x86, and mov ebx,0 would be less efficient.
                      ;; leaving out the zeroing of ebx would mean we exit(1), i.e. with an error status, since ebx still holds 1 from earlier.
    mov     eax,1         ; put __NR_exit into eax
    int     0x80          ;Execute the Linux function

section     .rodata       ; Section for read-only constants

             ;; msg is a label, and in this context doesn't need to be msg:.  It could be on a separate line.
             ;; db = Data Bytes: assemble some literal bytes into the output file.
msg     db  'Hello, world!',0xa     ; ASCII string constant plus a newline (0x10)

             ;;  No terminating zero byte is needed, because we're using write(), which takes a buffer + length instead of an implicit-length string.
             ;; To make this a C string that we could pass to puts or strlen, we'd need a terminating 0 byte. (e.g. "...", 0x10, 0)

len     equ $ - msg       ; Define an assemble-time constant (not stored by itself in the output file, but will appear as an immediate operand in insns that use it)
                          ; Calculate len = string length.  subtract the address of the start
                          ; of the string from the current position ($)
  ;; equivalently, we could have put a str_end: label after the string and done   len equ str_end - str
 

Linux에서는이 파일을 Hello.asm 으로 저장하고 다음 명령으로 32 비트 실행 파일을 빌드 할 수 있습니다.

nasm -felf32 Hello.asm                  # assemble as 32-bit code.  Add -Worphan-labels -g -Fdwarf  for debug symbols and warnings
gcc -nostdlib -m32 Hello.o -o Hello     # link without CRT startup code or libc, making a static binary
 

32 또는 64 비트 정적 또는 동적 링크 된 Linux 실행 파일, NASM / YASM 구문 또는 GNU as 지시어가있는 GNU AT & T 구문에 대한 어셈블리 빌드에 대한 자세한 내용은 이 대답 을 참조하십시오. (요점 : 64 비트 호스트에서 32 비트 코드를 빌드 할 때 -m32 또는 동등한 것을 사용하십시오. 그렇지 않으면 런타임에 혼란스러운 문제가 발생합니다.)

strace 로 실행을 추적하여 시스템 호출을 볼 수 있습니다.

$ strace ./Hello 
execve("./Hello", ["./Hello"], [/* 72 vars */]) = 0
[ Process PID=4019 runs in 32 bit mode. ]
write(1, "Hello, world!\n", 14Hello, world!
)         = 14
_exit(0)                                = ?
+++ exited with 0 +++
 

stderr의 추적과 stdout의 일반 출력은 모두 여기 터미널에 보내 지므로 시스템 호출 write 시스템을 방해합니다. 관심있는 경우 파일로 리디렉션하거나 추적합니다. 이 방법을 사용하면 syscall 반환 값을 인쇄 할 코드를 추가하지 않고도 쉽게 볼 수 있으며 실제로는 일반 디버거 (예 : gdb)를 사용하는 것보다 훨씬 쉽습니다.

이 프로그램의 x86-64 버전은 매우 유사 할 것이며 동일한 args를 동일한 시스템 호출에 전달할뿐입니다. 다른 레지스터에 있습니다. 그리고 int 0x80 대신 syscall 명령을 사용합니다.