6 minute read

강의 목표

  • 먼저 노이만형 컴퓨터의 기초를 아는 것
    • 명령이나 프로그램, 기계어란 무엇인가
    • 단순한 CPU의 구조와 동작
    • 이를 간단한 예시를 들어 설명
  • C언어와의 대응
  • 실제 명령 세트의 예



명령과 프로그램, 기계어란 무엇인가?


프로그램

  • 프로그램: 계산의 순서를 나타낸 것
    • 실체: 메모리 위에 존재하는, 계산 방법을 지시하는 숫자의 열
  • 노이만형 컴퓨터
    • 프로그램에 따라 계산하는 커퓨터
    • 메모리에 수납되어 있는 명령을 꺼내 순서대로 실행
    • 다른 형식도 존재하지만, 노이만형 컴퓨터가 현재는 주류
  • ex: A + B - C라는 연산
    1. A와 B를 더함 add A, B -> D
    2. 1번의 결과에서 C를 뺌 sub D, C -> E
    3. 이를 변환 규칙 표로 작성하면

      意味 add sub A B C D E
      数字 0 1 2 3 4 5 6
    4. 0, 2, 3, 5라는 수열은 A와 B를 add한 값이 D라는 뜻
    5. 즉 앞에서부터 차례대로 숫자를 읽어, 변환 규칙에 따라 계산함


프로그램의 표현과 용어

  • 이진수 バイナリ
    • 0, 2, 3, 5와 같이 계산 방법을 나타내는 수열
    • 컴퓨터가 직접 이해할 수 있는 것은 이진수뿐
  • 어셈블리어 アセンブリ言語
    • add A, B -> D와 같이 이진수와 1:1 대응하여 기본적으로 상호 변환이 가능
    • 이진수를 인간이 읽기 쉽도록 한 것
  • 기계어
    • 상기의 이진수 또는 어셈블리 언어로 표현된 프로그램
  • 명령
    • 컴퓨터가 해석 가능한 프로그램 내 계산 순서의 최소 단위
    • 0, 2, 3, 5, add A, B -> D
  • 연산자(opcode) オプコード
    • 명령으로 어떤 연산을 할 것인지 지정하는 부분
    • 0, 2, 3, 50, add A, B -> Dadd 부분
  • 피연산자(operand) オペランド
    • 계산의 대상이 되는 부분
    • 0, 2, 3, 52, 3, 5, add A, B -> DA, B -> D 부분
    • 입력이 되는 부분을 소스 ソース, 출력이 되는 부분을 디스티네이션 ディスティネーション이라고 부름


명령 세트 아키텍처

  • 이진수의 숫자와 실제로 수행할 계산의 규칙을 정한 것
    • 어떤 연산을 지원할 것인가? → “add, sub …”
    • 이진수의 각 숫자에 어떤 의미를 부여할 것인가? → “0이면 add”
    • 숫자의 순서가 가지는 의미 → “처음 한 자리는 연산 종류, 그 다음은 입력 값…”
    • 각 숫자에 몇 자릿수(비트)를 할당할 것인가? → “10진수로 한 자리씩”
  • 규칙은 컴퓨터(CPU)의 종류에 따라 다름
    • 이 룰을 명령 세트 아키텍처라고 부름
    • 프로그램의 호환성이란 상기의 룰이 동일한 것을 의미함
      • CPU 이외에도 OS 등의 요소도 호환성에 영향을 미치긴 하나, 가장 중요한 것은 CPU



단순한 CPU의 구조와 동작

컴퓨터

  • 컴퓨터란 ‘프로그램 = 명령의 열 = 숫자의 열’에 따라 계산하는 기계
  • 컴퓨터의 구성 요소
    • CPU
      • 연산기
      • 레지스터
      • PC
    • 메모리


메모리

  • 메모리란 명령열과 계산할 데이터를 저장함
    • 단일의 거대한 배열
    • C언어의 배열은, 이를 잘개 쪼개어 유저에게 보여 주고 있음
  • 숫자가 저장되는 작은 상자가 여러 개 나열된 이미지
    • 어드레스: 상자의 주소, 번호
    • 데이터: 상자 안에 든 내용물의 숫자


CPU

  • 컴퓨터의 중심이 되는 부분
    • 메모리로부터 명령을 읽어 와 계산함
  • 구성 요소
    • 연산기(FU, Functional Unit)
      • 가산기나 AND 연산기 등
      • 지시받은 종류의 연산을 수행함
    • 레지스터, 파일
      • 메모리와 비슷한 기능을 수행, 데이터를 기억함
        • 위치를 지정해 읽거나 쓸 수 있음
      • CPU의 계산은 레지스터 위에서 이루어짐
    • PC(Program Counter)
      • 현재 실행하고 있는 명령의 어드레스를 기억하는 곳


  • CPU의 명령 처리 단계
    • 초기 상태
      • PC의 어드레스는 0을 가리키고 있음
      • 레지스터의 초기 값은 1, 2, 3…
      • 메모리의 0번지에는 0235(add A, B -> D), 1번지에는 1546(sub D, C -> E) 1. 명령 읽어 오기(fetch)
      • PC가 가리키고 있는 어드레스의 메모리의 번지를 읽음
      • 해당 메모리의 내용인 0235를 가져 옴
      • CPU 내에 저장함 2. 명령의 해석(decode)
      • 0235의 의미를 해석함
        • 0: add
        • 2: 레지스터 A를 읽음
        • 3: 레지스터 B를 읽음
        • 5: 결과를 레지스터 D에 씀
          1. 레지스터 읽어 오기
      • 디코드의 결과에 따라 A와 B를 레지스터로부터 읽어 옴
      • 그 내용인 12를 가져 옴
      • 0235는 레지스터를 읽어 올 장소를 가리키고 있음에 주의할 것 4. 연산 실행
      • 연산기(FU)로 더하기 연산을 실행 5. 레지스터에 결과를 저장
      • D에 결과인 3을 저장 6. 다음 명령으로
      • PC에 1을 더한 후, 위의 과정을 반복


  • 기타 명령
    • 곱셈, 나눗셈, 논리 연산 등: 덧셈, 뺄셈과 동일한 방식으로 작동함
    • 메모리에의 읽기 및 쓰기
      • 로드 명령: 메모리로부터 데이터를 읽어 오는 것, ld: load
        • ld(A) -> D: A의 내용이 가리키고 있는 메모리의 주소를 D에 읽어 들임, 이때 A는 C언어의 *A와 같음
      • 스토어 명령: 메모리에 데이터를 써 넣는 것, st: store
        • st D -> (A): A의 내용이 가리키고 있는 메모리의 주소에 D(의 결과)를 써 넣음
    • 제어 명령
      • PC에 1을 더하는 대신, 임의의 값을 써 넣는 것
      • 점프 명령: 프로그램 내 임의의 장소로 이동함
        • j N: PC에 N의 값을 써 넣어, 다음 실행에는 어드레스 N에 있는 명령이 실행됨
      • 분기 명령: 조건에 따라 프로그램 내 임의의 장소로 이동함
        • b A < B, N: 레지스터를 2개 읽어, A < B라면 N으로 이동함
    • 레지스터 값의 변경(즉치 即値 immediate value)
      • 다른 명령어는 명령어 안의 숫자를 레지스터의 위치로 해석하지만, 즉치 명령은 명령어 안의 숫자를 직접 레지스터에 기록함
      • 레지스터의 초기값 설정 등의 목적으로 사용됨
      • li 2 -> D: 2라는 값을 레지스터 D에 써 넣음


  • 메모리와 레지스터
    • 레지스터는 필수적인 존재는 아님
    • 명령어에 나오는 레지스터 지정(A, B, C…)을 메모리의 주소라고 생각하면 됨
    • 그러나 메모리는 용량은 크지만 속도가 느리기 때문에, 용량은 작으나 고속인 레지스터를 준비하여 한 번 사용한 값을 레지스터에 저장하여 두 번째 접근부터는 고속으로 접근할 수 있도록 함(한 번 사용한 데이터는 다시 사용할 가능성이 높기 때문)



C언어와 기계어의 대응

  • 컴파일러의 처리
    • C언어로 작성한 명령문을 그에 대응하는 기계어로 변환하는 것
    • 기본적으로는 패턴 매칭(고수준 언어의 문장을 분석하고 내부 표현으로 변환할 때, 특정 형태의 코드 구조를 인식해 대응하는 처리나 명령을 선택하는 과정)


C언어의 반복문으로 보는 기계어 대응

// 일반적 반복문
for (int i = 0; i < 10; i++) {
    // 루프 본문
}

// goto문을 이용해 재작성한 반복문
i = 0;           // 초기화
LABEL:           // 루프 시작 지점
i = i + 1;       // 카운터 갱신
if (i < 10)      // 조건 검사
    goto LABEL;  // 조건을 만족하면 다시 LABEL로 이동
  1. 준비 단계: 변수와 명령의 할당
    • 가상의 메모리를 상정
      • 메모리의 하나의 요소가 4바이트라고 상정
      • 각 요소에는 어드레스가 존재
      • 하나의 요소가 4바이트인 거대한 배열이 하나 존재한다고 생각해도 좋음
      • 변수와 명령을 이 메모리 위에 배치함
    • *변수 i는 메모리의 0x0f4 번지에 할당되어 있음
      • 전역 변수라고 생각할 것
      • 변수는 하나당 4바이트라고 가정한다
      • 주소는 임의로 정한 것이며, 주소 숫자 자체에는 특별한 의미 없음
    • 명령어는 0x400 번지부터 시작한다고 가정
      • 즉, 메모리 상에서 변수와 명령어가 따로 떨어진 공간에 저장됨
      • 명령어도 하나당 4바이트로 취급한다


  1. 1행째(i = 0;): 변수 i0을 대입
// 레지스터 A에 0을 넣는다 
0x400: li 0 → A  

// B에 0x0f4 (변수 i의 주소)를 넣는다
0x404: li 0x0f4 → B  

// A의 값을 (B)가 가리키는 메모리에 저장한다
0x408: st A → (B)
  • 전역 변수의 갱신은 기본적으로 변수의 주소를 li 명령으로 읽고, 해당 주소에 store 명령으로 값을 저장하는 방식으로 이루어짐
    1. 저장할 값(0)을 A 레지스터에 준비
    2. 저장할 위치의 주소(0x0f4)를 B 레지스터에 담음
    3. st 명령을 통해 A의 값을 B가 가리키는 메모리에 저장


  1. 2행째(LABLE:): 3행째부터의 명령은 0x40c부터 시작하므로, LABEL = 0x40c 라고 기억해 둠


  1. 3행째(i = i + 1;): 변수 i의 값을 1 증가시키는 연산
// B에 0x0f4 (i의 주소)를 넣는다
0x40C: li 0x0f4 → B

// (B)에 있는 값을 A에 읽어 온다
0x410: ld (B) → A

// 1을 더한다
0x414: add A,1 → A

// A의 값을 (B)에 기록한다
0x418: st A → (B)


  1. 4~5행째(if (i < 10); goto LABEL;): 루프의 계속 판정과 점프
// B에 10을 읽어 온다
li 10 → B

// 직전의 덧셈 결과가 남아 있는 A와 비교해서, 조건이 성립하면 LABEL(0x40C)로 점프
b A < B, 0x40C

// 조건이 성립하지 않으면, 이후의 명령으로 진행


  • 전체 연산을 정리하면 아래와 같음
// 1: i = 0;
0x400: li 0 → A       // 레지스터 A에 0을 넣는다
0x404: li 0x0f4 → B   // B에 0x0f4 (변수 i의 주소)를 넣는다
0x408: st A → (B)     // A의 값을 (B)에 저장 (= i를 0으로 초기화)

// 2: LABEL: 
// 3: i = i + 1;
0x40C: li 0x0f4 → B   // B에 0x0f4 (i의 주소)를 넣는다
0x410: ld (B) → A     // (B)에서 값을 읽어 A에 저장 (= i 값을 읽는다)
0x414: add A,1 → A    // A에 1을 더한다 (= i + 1)
0x418: st A → (B)     // A의 값을 (B)에 저장 (= i 값을 업데이트)

// 4: if (i < 10) 
// 5: goto LABEL;
0x41C: li 10 → B      // B에 10을 넣는다
0x420: b A < B, 0x40C // A가 B보다 작으면 0x40C(LABEL)로 점프 (= i < 10일 경우 반복)


C언어로의 변환(컴파일러)

  • 기본적으로는 한 문장씩 기계어로 치환해 나감
    • 실제로는 위의 결과보다 더 최적화됨
    • 예시로, 변수 i를 매번 메모리에서 읽고 쓰는 동작을 생략함
    • 단, 디버깅용으로 컴파일된 코드는 앞의 예시와 비슷함(디버거에서 한 줄씩 실행해 보기 위해서는 원래 문장과 1:1로 대응되는 것이 더 보기 편하기 때문)
  • 변수, 배열, 구조체 접근
    • 변수의 등장
      • x*(&x)
    • 배열
      • a[i]*(a + i)
    • 구조체
      • s.m*(&s + offset)
      • sp->m*(sp + offset)
  • if ~ else, for, while, do ~ while, switch ~ break, continue, goto 구문은 기본적으로 모두 if ~ goto를 이용해 변환 가능
  • return은 앞에서 설명한 내용만으로는 구현이 불가능하고, 점프할 떄 PC가 돌아갈 어드레스를 저장하는 명령이 따로 필요함