C, Assembly, machine

msung99·2022년 9월 24일
0
post-thumbnail


Intel x86 Processors

  • CISC

C, Assembly, machine code

Architecture : processor 디자인을 통칭하는 것

  • 이명 : ISA(Instruction Set Architecture)

  • 실제 하드웨어가 어떻게 생겼는지의 설계구조뿐 아니라, CPU 를 돌리기위해 모아놓은 Instruction Set(명령어 집합) 을 포함한 개념이다.

=> 어셈블리 코드를 읽거나 쓰는 데 필요한 프로세서 설계 부분을 의미
ex) 예: 명령어 집합 구분 및 레지스터

Microarchitecture

  • Architecture 를 구현해 놓은것
  • ex) 그 안에 어떻게 구현을 했는지, 캐쉬(cache) 사이즈는 어떻게 했고, core frequeency 가 몇 기가가 되려면 어떤 구조를 가져야하고 등등

Code forms (코드 형식)

  • Machine code(기계어) : process 가 실행하는 byte level 의 프로그램
  • Assembly code(어셈블리어) : machine code 를 텍스트로 표현해 놓은것

cf) Machine Code(기계어) 란?
=> 컴퓨터가 사용하는 언어. 아래와 같이 2진수화된 숫자(2진수 숫자)로 구성되어 있다. (좀 더 정확히말하면, 컴퓨터 CPU가 명령을 처리할 때 사용하는 언어)

0000 1011 1000 1111 0000
0010 0011 1010 0010 0100

Assembly Code(어셈블리어) 란?
=> 기계어도 프로그래밍 언어라서 프로그래밍이 가능하나, 위와 같이 나열된 기계어 코드를 나열하면 일반적인 사람들이 이해하거나 수정하는 작업이 힘들 것이다. 그래서 기계어 숫자를 이해하기 쉬운 단어로 바꿔서 사람들이 쉽게 이해하도록 만든 프로그래밍 언어


Assembly/Machine Code View

CPU 구성요소

1) PC (Program Counter) : 기계어나 어셈블리어로 명령어를 한줄씩 차례대로 수행해나가야 하는데, 이때 다음으로 실행될 명령어를 가리키는 포인터 역할을 한다

  • 다음에 실행될 명령어의 주소를 가지고 있어 실행할 기계어 코드의 위치를 지정한다. 때문에 명령어 포인터라고도 한다. .

2) ALU

  • 진한 파란색 부분은 CPU의 ALU 라고 보면된다.
    cf) ALU 가 뭐였지? (예전에 배웠던 내용이다! 아래 그림 참고!)

3) Register file : 여러개의 register 들을 모아놓은 것

4) Condition code : 가장 최근의 산술(arithmetic) 연산 또는 논리 연산에 대한 상태를 저장

  • CPU의 명령 실행 결과를 표시하는 부호.
  • Conditional Branching (조건 분기) 를 하는데 활용됨
    • 조건 분기란? : 특정한 조건이 참일 경우에만 새로운 주소로 분기하는 것

Memory 구성요소

  • 메모리는 일단 종류가 DRAM 임

  • code, data, stack 으로 구성

  • stack : 우리가 매번 말하던 "힙, 스택" 얘기할때의 그 스택이 맞다!
    => data 는 사용자가 넘겨주는 데이터이고, stack 은 프로그램이 돌아가면서 생기는 데이터를 축적하는 곳이다.

cf) 왜 힙(heap) 은 Memory 에 없지..? => 힙은 logical 개념이라, 물리적으로 무언가를 해줘야한다는 개념이 없다. 스택은 반면 LIFO 라는 물리적 특징을 가지고 있어서 그림 설명에 넣었음!

정리

  • CPU 와 Memory 사이에서 data address instruction 을 해주면 데이터가 왔다갔다 한다!

C언어 코드를 실행하면 무슨일이 벌어질까?

p1.c 와 p2.c 라는 실행파일을 gcc 컴파일러로 컴파일 하는 과정

1) Compiler(gcc -On -S) : gcc 컴파일러가 p1.c, p2.c 파일을 컴파일을 한다.

  • 사용자가 -s 옵션을 붙이지 않았음에도, gcc가 알아서 -s 옵션을 붙인다.

  • 컴파일러의 실행 결과(output) 은 어샘블리어가 된다.

  • 또한 실행파일 확장자가 .c (C program) 였던것들이 모두 .s 로 (asm program) 변한다.

2) Assembler(gcc or as) : 어샘블리어 명령어들을 계속해서 컴파일한다.

  • Assembler 라는 컴파일러가 컴파일을 시도하면 .s 실행파일이 확장자가 .o (object program) 로 변한다.

  • 이전까지의 파일은 텍스트 파일이였으나, 여기서 부터는 object 파일을 열어보면 binary 파일로 변한다.
    (즉, 컴퓨터가 이해할 수 있는 기계어로 변환됨)

3) Linker(gcc or Id)

  • linker 라는 놈이 마지막으로 실행파일을 만든다.

  • 여기까지 왔으면 이미 각 실행 파일들이 컴퓨터가 이해할 수있는 기계어 코드이므로, linker 가 우선순위나 dependency 등을 코드를 읽고 파악해서 처음부터 끝까지 줄을 세워준다.

  • static library (.a) 를 포함 => 라이브러리는 static 과 dynamic 이라는 2가지 형태가 있다.

    • static (linking) 라이브러리는 컴파일 시간에 링크를 해주는 애들

      • link 단계에서 라이브러리 (*.lib 파일)를 실행 바이너리에 포함시킨다.
    • dynamic (linking) 라이브러리는 말그대로 "동적으로 링크하여 사용하는 라이브러리이다.

      • 이를 사용하고자 하는 실행 바이너리에서 필요시 사용할 수 있도록 최소한의 정보만 포함하여 링크한다. 나중에 실제로 프로그램이 돌아갈때 그떄서야 object 코드가 필요하다고 하면 찾는 것이다.

4) executable program(p)

  • static 라이브러리와 object 프로그램들을 다 모아서 링크를 해주면 우리가 최종적으로 실행하고 싶은 실행파일인 p 가 만들어진다!

Compliling into Assembly

  • 주의 : 같은 c 코드를 어셈블리 코드로 컴파일해서 바꾸면, 높은 확률로 같은 거리에서 시간에 컴파일하지 않는이상 어셈블리 코드 결과가 다르게 나온다.

=> 어셈블리어 까지만 내려와도 굉장히 specific 한 프로그래밍 코드여서,
c언어와 같이 (나름) high-level 이며 추상적인 코드를 어셈블리와 같은 low-level 이 변환 및 구현할 수 있는 방법은 굉장히 다양하다!


어셈블리어의 데이터 타입

  • 정수형(integer) 타입 : 1,2,4,8 byte 짜리가 존재

    • 데이터 값이나 주소값을 저장하는데 사용
  • float 형 타입 : 4,8,10 byte 짜리가 존재

  • 유의 : strcture 나 배열이 없다!

  • 포인터와 같이 비슷한 참조 데이터 타입이 어셈블리에 있긴하다.


어셈블리어의 연산

  • 어셈블리는 메모리와 CPU 의 register 사이에 데이터를 옮기고 주고받고 하는것과 관련한 연산밖에 없다.

1. 메모리와 CPU의 reigster 사이에 데이터를 주고받는 연산

1) load : 메모리에서 CPU의 register 로 데이터를 이동하는 연산

2) store : register 에서 메모리로 데이터를 이동하는 연산


2. Transfer control : 프로그램의 의사 결정 기능을 구현하는데 이용되는 명령어

  • 프로그램의 실행 흐름을 프로그래머 임의로 바꾸는 명령어이다.

1) unconditional jump - to - from : c언어의 goto와 유사. 원하는 구문으로 바로 jump 함
2) conditional branch


Object code(확장자가 .o인 object 파일의 sumstore 연산 예시) - sumstore

sumstore : x 와 y 를 더해서 dest 에다 결과를 넣는것

sumstore 의 어셈블리 명령어 코드

  • 위 코드는 Assembler 가 기계어로 변환 완료한 명령어 코드이다.
    • 즉 sumstore 코드가 담긴 파일이 확장자가 .s 에서 .o 로 바꾼 소스파일 결과이다.
  • 14바이트이고, 시작주소는 0x0400595 이다.

기계어 예시

  • c언어 코드가 c언어 => 어셈블리 코드 => object 코드로 변환되는 과정을 살펴보자

1) c언어 코드

  • *dest = t; : dest 라는 포인터가 가리리는 메모리에 t 라는 값을 저장하라는 명령

위 c코드를 어셈블리 코드로 변환하면 아래와 같다.

2) 어셈블리 코드

  • movq &rax, (%rbx)

=> rbx 라는 이름의 register에 가서 rax 의 값을 할당(저장)하라는 명령

c언어 코드와 어셈블리 코드 비교

  • t => %rax 라는 이름의 register
  • dest => %rbx 라는 이름의 register
  • *dest : 메모리

3) object 코드

  • 0x40059e
    => 어셈블리 코드가 기계어 코드(object 코드) 로 변환된 결과

Disassembling Object Code

  • Assemble이 컴파일러가 어셈블리 코드를 binary 코드(기계어 코드)로 바꾸는 역할을 수행하는 것이라면,
  • Disassemble 은 반대로 binary 코드를 어셈블리 코드로 바꾸는 역할을 수행하는 것이다. (object jump)

예시

  • 위 예시는 Disassemble 을 통해 object 코드(기계어. 즉, 바이너리 코드)를 어셈블리 코드로 바꾸는 예시이다.

GDB 툴로 Disaseemble 하기

  • 직전에 살펴본 object jump 방법 외에도 GDB 라는 툴로 Disaasemble 을 수행할 수 있다.

  • $gdb sum : GNU 라는 툴로 sum 이라는 실행파일을 열고,

  • (gdb) disassemble sumstore : disassemble 라는 키워드와 함께 함수 이름(sumstore) 을 넣으면, sumstore 함수의 object 코드가 어셈블리 코드로 변환된다.

  • (gdb) x/14xb sumstore

cf) gdb 사용법은 현업에 나가서 배우자!


어셈블리 기초

64비트짜리 CPU의 register 종류

  • 우리가 평소에 사용하는 64비트 짜리 CPU의 register 종류가 16종류가지 밖에 없다.

=> 8바이트짜리 register 가 16개 달려있다. 각 register 의 이름은 %rax 와 같이 하드코딩 되어있는 이름이기 떄문에, 어셈블리에서 특정 register 를 쓰고 싶다면 rax, rbx, rcx, ... 와 같이 불러줘야 한다.

cf) 빨간색으로 칠해진 특수한 register 는 시스템에서 사용하는 것이지, 우리가 사용하면 안되는 register 이다!


1A32 register 와 비교 (=> 32비트짜리 옛날 컴퓨터의 register)

  • 32비트짜리 register 8개가 끝이다!

movq - 데이터 move 시키기

형태 : movq src, dst

  • src(데이터 출발지. source)
  • dst(도착지. destination)
    => src, dst 등을 c언어에서는 argument(인자)라고 불렀지만, 어셈블리에서는 Openands 라고 부른다.

Openands 로 올수있는 종류

1) Immediate (정수형 상수) : ex) $Ox400

2) register : ex) %rax
=> cf) %rsp 라는 register 는 특수한 register로, 시스템에서 사용하는 레지스터라서 프로그래머가 접근 및 사용 불가능하다!

3) memory : ex) (%rax)
=> 형태가 register 밖에다 괄호 "( )" 를 쳐주면 끝!


movq 명령의 가능한 모든 Openands 조합들

  • source 에 Imm(Immediate) 가 할당된 경우
    => destination 에 register 또는 memory 가 할당될 수 있다.

  • source 에 Reg(register) 가 할당된 경우
    => destination 에 register 또는 memory 가 할당될 수 있다.

  • source 에 Mem(memory) 가 할당된 경우
    => destination 에 register 가 할당될 수 있다.

정리

  • Immediate (상수) 를 어딘가(destination)에 move 해야하는데 상수라는 곳에 move 시킬 수 없으니까, register 또는 memory 에 상수를 옮길 수 있는 것이다.

  • 또한 memory to memory 연산이 없다.
    => 어셈블리 입장에서는 memory 에 있는 데이터를 다른 memory 에 옮길 필요가 없기 때문에 문법적으로 허용x. 어셈블리의 역할은 메모리에 있는 데이터를 CPU 의 register 에 가져다줘서 어떤 명령(instruction) 을 시키는 것이다!

profile
블로그 이전했습니다 🙂 : https://haon.blog

0개의 댓글