libasm

Bigmountain·2021년 4월 13일
0

42Seoul

목록 보기
7/7

subject

  • 어셈블리 언어로 함수 작성하기.

subject 이해하기

  • You must write 64 bits ASM. Beware of the "calling convention".

    : ISA(Instruction Set Architecture)는 명령어 종류, 피연산자 타입, 레지스터 개수, 인코딩 방법 등 여러 가지를 정의한다.
    그외에 실제로 프로그램이 돌아가려면 ISA 위에 ABI(Application Binary Interface)라는 응용프로그램과 운영체제 사이의 약속도 필요하다.
    함수호출규약(calling convention)이나 바이너리 포맷에 대한 규약이 대표적이다.
    출처: 프로그래머가 몰랐던 멀티코어CPU 이야기

    • 64bit레지스터를 가진 CPU가 처리할 수 있는 어셈블리어 규칙에 맞게 작성해라.
    • 함수 호출 규약은
      • 인자 전달 방법,
      • 인자 전달 순서,
      • Stack Frame을 정리하는 방법 에 따라 그 종류를 구분 한다.
    • C언어에서는 cdecl규칙을 따른다.참조
    • 함수 호출 규약에서 추가적으로 신경써야하는 부분은 CPU사양(32bit, 64bit)과 운영체제에 따라 함수 처리시 활용되는 레지스터의 차이가 있다는 점이다.
    • 즉, CPU가 일하면서 쓰는 레지스터의 수나 명칭 등등이 다르다.
  • You can’t do inline ASM, you must do ’.s’ files.

    • 인라인 ASM?(인라인 어셈블러)
      • C프로그램 안에 어셈블러 명령어를 삽입하는 것.
      • 인라인 어셈블러로 속도향상 및 메모리절약할 수 있다.
      • 참조
    • s 확장자는 어셈블리코드를 나타내는 확장자
      • gcc -S main.c 하면 main.s가 어셈블리코드로 만들어짐.
      • S옵션 의미
// https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html#Overall-Options
-S

Stop after the stage of compilation proper; do not assemble. The output is in the form of an assembler code file for each non-assembler input file specified.

By default, the assembler file name for a source file is made by replacing the suffix ‘.c’, ‘.i’, etc., with ‘.s’.

Input files that don’t require compilation are ignored.
 
  • You must compile your assembly code with nasm.
    • nasm이란 어셈블러이자 역어셈블러이다.
    • 어셈블러 = 어셈블리어를 기계어(이진코드)로 바꿔줌
    • 역어셈블러 = 기계어를 어셈블리어로 바꿔줌
    • 위키
  • You must use the Intel syntax, not the AT&T.
    • AT & T와 Intel의 차이점은 어셈블리어를 표현하는 문법의 차이이다.
    • Intel문법을 사용하란 소리.
  1. 64bit 레지스터를 가진 CPU가 처리할 수 있는 어셈블리어를 intel문법으로 작성하여라.
  2. 이 때 함수 호출 규칙을 지켜야 한다. c언어에 맞는 호출 규칙을 지키자.
  • 함수가 실행되면서 처리되는 레지스터는, 42환경에 맞춰야겠지?(macOS, 64bit)

어셈블리어 이해하기

기계어 ?

기계어 정의

  • cpu가 바로 해독할 수 있는 유일한 언어.
    • 모든 프로그래밍 언어는 기계어로 해석되어야 비로소 수행할 수 있다.
    • 비트 단위로 표현하기 때문에 0과 1로만 표현한다.

엄밀히 말하면 특정한 언어가 아니다 ??

  • 단지 cpu또는 mpu제조사에서 cpu를 만들어 낼 때 해당 cpu에서 사용하는 명령어 집합을 공개하는데, 이것을 '기계어'라고 부를 뿐이다.
  • 따라서 cpu 종류, 아키텍처에 따라 같은 동작을 수행하는 코드라도 완전히 다른 0과 1이 나열 될 수 있기 때문에 이식성은 좋지않다.
  • 이러한 문제를 커버하기 위한 것이 어셈블리어 이다.

어셈블리 언어(assembly language)

  • 모든 기계어와 1:1 대응이되는 컴퓨터 프로그래밍 언어.
  • 즉, 컴퓨터 프로그래밍에서 기계어는 대부분 어셈블리어를 거쳐 짜여진다.
    • 어셈블리어가 없었을 때는 직접 기계어를 입력하여 프로그램을 작성하기도 했다.. (ㄷㄷㄷ..)
  • 프로그래밍 언어 중 가장 low한 레벨의 언어이다. (low하다 => 컴퓨터와 가깝다.)

High Level Languages

  • 그러나, 어셈블리어도 아직은 이해하기 어려운 부분이 많다. 그래서 생산성을 높이기위해 이해하기 쉬운 프로그래밍 언어들이 개발되었다.
  • 이해하기 쉽게만드려면, 추상화 수준을 높여야 한다.
  • 즉, 특정 기능을 수행하기 위한 코드하나에 함축적인 의미(내부적으로 처리되는 코드)가 늘어난다는 의미이다.
    • 추상화를 높인 한 줄의 코드에는 수천만개 이상의 기계어 코드가 함축되어 있을 수 있다.
  • Higher level and lower level languages

어셈블리 작성하기.

어셈블리 코드 기본문법

  • 어셈블리 코드는 섹션을 나누어 작성한다.
    (ex. section.data, section.bss ... )
    • data
      • 초기화 데이터 및 상수 선언하코드
      • 런타임에 변경되지 않는다.
      • 다양한 상수 값, 파일 이름 또는 버퍼 크기 등을 선언 가능.
    • bss
      • 변수 선언에 사용
    • text
      • 실제 코드를 유지하기위해 사용.
      • 프로그램 실행이 시작되는 곳을 커널에 알려주는
        global _start선언으로 시작해야 함.
            section.text
               global _start
            _start:
  • 주석은 세미콜론(;) 뒤에 작성
  • 구문
    • 어셈블리 코드는 한 줄에 하나씩 입력된다. 각 문은 다음 형식을 따름.
      [label]   mnemonic   [operands]   [;comment]
  • 구문 설명
    • 대괄호는 선택
    • 기본은 두 부분으로 구성.
      • 첫 번째는 실행될 명령어 (또는 니모닉)의 이름
      • 두 번째는 명령의 피연산자 또는 매개 변수.
  • 기본구문 예시
INC COUNT        ; Increment the memory variable COUNT

MOV TOTAL, 48    ; Transfer the value 48 in the 
                 ; memory variable TOTAL
					  
ADD AH, BH       ; Add the content of the 
                 ; BH register into the AH register
					  
AND MASK1, 128   ; Perform AND operation on the 
                 ; variable MASK1 and 128
					  
ADD MARKS, 10    ; Add 10 to the variable MARKS
MOV AL, 10       ; Transfer the value 10 to the AL register

intel 문법

구문

  • 매개변수 순서
    • destination이 먼저, source가 나중에
      명령어 dest source
  • 매개변수 크기
    • 매개변수의 크기를 나타내기 위해서 byte, word, dword같은 지시자를 사용함.
    • 참조
    • 참조
  • 접미사, 접두사 규칙
    • 대체로 접두사, 접미사가 붙지않음
    • 16진수와 2진수에 'h'와 'b'접두사를 가짐.
    • 참조
  • 메모리 주소
    • []로 표현

어셈블리 명령어

  • x86 instruction
    • push [reg] => register의 값을 stack에 저장.
    • pop [reg] => stack의 값을 pop하여 register에 저장.

어셈블리 코드에 사용되는 레지스터

  • x86 아키텍처 Register - 32bit 기준 설명
    • 범용 레지스터 기본구성
  • x86-64 아키텍처 Registers - 64bit 기준 설명
    • 범용레지스터가 32bit와 어떻게 다른지?
      • 구성은 동일함. but 접두사 R을 붙여 구분.(64bit)
      • 접두사(E)를 붙이면 32bit레지스터로, 안 붙이면 16bit로 접근
    • 64bit에서는 사용하는 레지스터가 더 많다. r8 ~ r15
    • Usage during syscall/function call
      • 링크에 기재된 레지스터가 활용되는 규약(?)이다. 숙지하기.
      • First six arguments are in rdi, rsi, rdx, rcx, r8d, r9d; remaining arguments are on the stack.
      • For syscalls, the syscall number is in rax.Return value is in rax.
      • The called routine is expected to preserve rsp,rbp, rbx, r12, r13, r14, and r15 but may trample any other registers

x86 Assembly Tutorial

예시

  • 기본문법만 사용한 예제. Hellow World 찍기
    • 여기서 _start:가 기본의 [label]에 해당 함.
    • [label]: 하위 전체 코드가 포인터가 된다.
; ----------------------------------------------------------------------------------------
; Writes "Hello, World" to the console using only system calls. Runs on 64-bit macOS only.
; To assemble and run:
;
;     nasm -fmacho64 hello.asm && ld hello.o && ./a.out
; ----------------------------------------------------------------------------------------

          global    start

          section   .text
start:    mov       rax, 0x02000004         ; system call for write
          mov       rdi, 1                  ; file handle 1 is stdout
          mov       rsi, message            ; address of string to output
          mov       rdx, 13                 ; number of bytes
          syscall                           ; invoke operating system to do the write
          mov       rax, 0x02000001         ; system call for exit
          xor       rdi, rdi                ; exit code 0
          syscall                           ; invoke operating system to exit

          section   .data
message:  db        "Hello, World", 10      ; note the newline at the end
instructionoperand설명
movrax, 0x02000004rax레지스터에 write함수를 호출하는 syscall번호(0x02000004) 저장
movrdi, 1rdi레지스터에 첫번째 인자 값1(표준 출력)을 저장. write(1, ?, ?) 상태.
rdi인 이유는 아키텍처에서 파라미터를 저장하는 순서가 rdi부터이기 때문
movrsi, message2번째 인자를 rsi에 저장.
message는 data section에 선언 및 정의 됨. 즉, write(1, message, ?) 상태
movrdx, 133번째 인자를 rdx에 저장.
정수 13은 문자열 크기. 즉, write(1, message, 13)을 호출할 수 있음.
syscallrax에서 호출할 함수를 가져온다(write).
필요한 인자 값은 저장한 레지스터 순서대로 가져온다.
(가져오면서, 해당 레지스터의 값은 pop되겠지?)
movrax, 0x02000001rax레지스터에 exit함수를 호출하는 syscall번호 저장.
xorrdi, rdirdi레지스터에 exit함수의 첫번째 인자 값을 저장.
rdi <- rdi xor rdi 가 된다. xor연산자이기 때문에 항상 0의 값을 가진다.
근데 왜 구지 xor썻는진 몰겠다.. 쨋든 exit(0)은 정상종료 의미.
syscallrax에서 호출할 함수를 가져온다.(exit)
역시 동일하게 인자값이 필요하면 레지스터에 저장하는 순서대로 가져오면 된다.
(0)을 rdi에 저장해주었으니 잘 가져 올 것이다.
message변수db는 declare Byte의미 -참조
, 10은 아스키코드 개행문자(line feed)를 의미한다.
message 변수는 "Hello, World" 뒤에 개행(10)을 붙여서 해석될 것이다.

궁금증

그냥 컴파일러로 만들 수 있지 않나?

  • 그러면, compiler로 64bit intel문법 어셈블리코드 만들면 끝 아닌가?
    • subject에서 요구하는 스펙(64비트, intel)으로 컴파일 하는 방법을... 못찾겠네 ?
    • gcc로 어셈블리코드 만드는 옵션은 -S
    • 그냥 -S해서나온 어셈블리코드는 무슨문법이지 ? (어떤 이유로 그 문법으로 컴파일 한거지?)
      • 지금 쓰고있는 컴파일러 옵션 따라가는 듯.. 어디서 하는진 못찾음 ㅡ..ㅡ
      • 현재 내 컴퓨터(Mac)에서는 clang/llvm 컴파일러 따라 컴파일 됨.참조
    • 그럼 64비트 intel문법 어셈블리 코드로 컴파일 하는 옵션 찾아봐야지..
      • 못찾겠네 ㅡ..ㅡ 안되는것같은데..
      • 링크 여기 있는대로 해도 안됨...... 옵션은 먹는데 그냥 -S랑 똑같네
  • 해당 포스팅을 보니깐, 디버거로 가능한 것 같다.

레지스터의 실체

push ebp의 의미?

  • push는 stack에 쌓는 명령어, 여기서 스택에 쌓인다는 의미는...?
    • 메인메모리(RAM)의 Stack영역에 데이터가 쌓인다.
    • CPU 레지스터의 값을 -> RAM으로 저장한다.
  • 그렇다면.. ebp를 쌓는다는건 무슨의미일까....
    => indirect모드로 base point의 주소 값을 push.
  • ebp(base point)의 값을 push하는 이유는 ??
    => 함수가 종료된 후, SP가 복귀할 위치를 지정하기 위함.
    1. 스택프레임에 이전 함수가 호출된 시작 위치를 저장한다.
    2. 함수가 종료되면, 사용한 메모리만큼 더하기(혹은 빼기)연산을 하는 것이아니라, 레지스터에 저장 된 위치로 한방에 복귀한다.
      => 즉, 현재 SP를 최근 함수호출 전 위치로 복귀 시키는 것.
    3. main -> fn1() -> fn2() 이런식으로 함수가 호출된다면
      • 최초 ebp레지스터에는, main함수가 호출 된 위치가 기록된다.
      • fn1() 호출 전 push ebp => stack에 main시작위치 저장.
      • 다음 mov ebp, esp 해준다.
        • 즉, ebp에 현재 위치를 저장해준다.
      • 다음 fn2() 호출 전 push ebp => stack에 fn1()이 호출된 메모리 주소를 저장.
      • 또 다시 mov ebp, esp 해준다.
      • 만약 fn(2)함수가 종료되었다면 ???
        • mov esp, ebp => 현재 sp값을 fn2() 호출한 위치로 변경
        • pop ebp => stack의 값을 pop해서 ebp에 넣어준다.
          • 즉, fn1()함수의 시작위치가 ebp에 담겨지게 된다.
        • 반복

___error 처리

문제점

M1 맥북에서 사용하기

참조 - 해당 링크 덕분에 답을 찾을 수 있었다..

  • M1칩의 아키텍처는 ARM기반이다. 따라서 gcc로 컴파일 수행시 생성되는 목적파일들은 ARM아키텍처로 생성되고 있다.
  • libasm과제에서 요구하는 인텔 문법의 어셈블리어는 x86아키텍처를 기반으로 한다.
  • 즉, 과제에서 요구하는 어셈블리어를 처리할 수 있는 CPU아키텍처에 맞춰야 한다.
  • 근본적인 하드웨어(cpu)의 아키텍처가 다른데.. m1맥북에서 x86 아키텍처로 실행시킬 수 있나?
  • 방법은 arch x86_64 [명령어] 와 같이 사용하면 된다.
    • 또한 실행파일이 어떠한 아키텍처 구조인지 명령어로 확인할 수도 있다.
    • lipo -archs 실행파일명

문제 해결과정

  • 목표 : 어셈블리 코드로 ft_strlen.s작성, c코드로 main.c 작성한 후 main에서 ft_strlen을 호출하기.
  1. 처음에 intel문법에 맞추어 ft_strle.s를 작성 후 nasm으로 목적파일 생성.
    nasm -f macho64 ft_strlen.s
  2. test를위해 main.c를 만들고, gcc -c main 하여 목적파일 생성.
  3. gcc명령어로 두 목적파일을 링크하기 위해 gcc *.o 수행
  • 아래 error 발생..
ld: warning: ignoring file ft_strlen.o, building for macOS-arm64 but attempting to link with file built for unknown-x86_64
Undefined symbols for architecture arm64:
  "_ft_strlen", referenced from:
      _main in main.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
  • 두 목적파일의 아키텍처 확인 결과 다름..
  1. main.c를 x86_64 아키텍처로 생성.
    arch -x86_64 gcc -c main.c
  2. x86_64아키텍처의 실행파일 생성
    arch -x86_64 gcc *.o
  3. 결과 확인

참조

  • 링커 옵션
    • 아래걸로주니깐 되긴하네 ?
ld hello.o -o hello -macosx_version_min 11.0 -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib -lSystem

주의

cmp 인자 주의

0개의 댓글