벡터 테이블
고친 사람 github-actions[bot]
벡터 테이블은 처리해 달라는 신호를 받은 프로세서에게 어느 코드로 건너뛸지 알려 줍니다. 신호마다 번호가 붙어 있습니다. 표는 번호마다 그 신호를 맡을 코드의 주소를 적어 둡니다. 프로세서는 번호로 표의 칸을 곧장 찾아 그 주소로 건너뜁니다.
쉽고 빠른 이해
신호 번호와 그 신호를 처리할 함수의 주소를 짝지어 둔 목록입니다. 타이머 신호가 3번이면 표의 3번 칸에 타이머를 처리하는 함수의 주소가 들어 있습니다.
이 표가 없으면 프로세서는 신호가 올 때마다 한 군데로만 갑니다. 거기서 코드가 무슨 신호인지 하나씩 확인하고 갈라 보내야 합니다. 급한 신호일수록 그 확인 시간이 아깝습니다.
어떻게 도나:
- 신호가 오기 전에 칸마다 함수 주소를 적어 둡니다
- 신호가 번호를 달고 들어오면 프로세서가 하던 일을 멈춥니다
- 프로세서가 번호로 칸을 계산해 주소를 읽고 그 함수로 건너뜁니다
- 함수가 끝나면 멈췄던 곳으로 돌아옵니다
대신 번호 수만큼 칸이 메모리를 차지합니다. 쓰지 않는 번호의 칸도 채워 둬야 합니다. 칸 하나가 틀리면 프로세서는 엉뚱한 코드로 건너뜁니다.
어떤 기계는 표 없이 입구 하나로 모든 신호를 받습니다. 메모리를 아끼는 대신 신호를 갈라 보내는 만큼 늦습니다.
상세
이 절은 표 한 장의 생김새를 봅니다. 신호 하나가 그 표를 거쳐 처리 코드에 닿는 과정도 따라갑니다. 칸이 다섯 개뿐인 작은 표 하나를 끝까지 예로 씁니다.
건물의 비상 연락판을 떠올려 봅니다. 불이 나면 1번, 누가 다치면 2번, 물이 새면 3번을 누르라고 적혀 있습니다. 누르는 사람은 누구에게 연락할지 고민하지 않습니다. 번호만 누르면 미리 정해 둔 사람에게 바로 닿습니다. 벡터 테이블이 프로세서에게 이 연락판 노릇을 합니다.
인터럽트와 핸들러
인터럽트는 프로세서가 하던 일을 멈추고 다른 일부터 처리하게 만드는 신호입니다. 키보드가 눌렸다거나 네트워크 카드에 데이터가 도착했다는 알림이 그렇습니다. 장치가 일을 마쳤을 때 프로세서가 계속 물어보지 않아도 알 수 있게 하려고 이 신호가 있습니다.
신호를 받은 프로세서는 그 신호를 맡은 함수로 건너갑니다. 이 함수가 핸들러입니다. 키보드 핸들러는 눌린 키를 읽어 옵니다. 네트워크 핸들러는 도착한 데이터를 버퍼로 옮깁니다. 신호마다 할 일이 달라서 핸들러도 신호마다 따로 둡니다.
그러면 프로세서는 들어온 신호에 맞는 핸들러가 메모리 어디에 있는지 알아야 합니다. 그 답을 적어 둔 표가 벡터 테이블입니다.
벡터라는 이름
여기서 벡터는 수학의 벡터나 벡터 검색에서 말하는 숫자 묶음이 아닙니다. 갈 곳을 가리키는 주소 하나를 이 분야에서는 벡터라고 부릅니다. 핸들러의 주소가 곧 그 신호의 벡터입니다.
신호마다 붙는 번호는 벡터 번호라고 부릅니다. 벡터 테이블은 벡터 번호 순서로 벡터를 늘어놓은 표입니다.
표의 모양
표는 같은 크기의 칸을 한 줄로 이어 놓은 배열입니다. 칸 하나에 핸들러 주소 하나가 들어갑니다. 칸의 순번이 곧 벡터 번호라서 번호를 칸에 따로 적어 두지 않습니다.
아래는 칸 다섯 개짜리 작은 표입니다. 칸마다 화살표가 가리키는 곳이 메모리에 놓인 핸들러입니다.
flowchart TD
subgraph T["벡터 테이블"]
V0["0번 칸 · 0으로 나누기 핸들러 주소"]
V1["1번 칸 · 모르는 명령 핸들러 주소"]
V2["2번 칸 · 기본 핸들러 주소"]
V3["3번 칸 · 타이머 핸들러 주소"]
V4["4번 칸 · 키보드 핸들러 주소"]
end
subgraph C["메모리에 놓인 핸들러"]
H0["0으로 나누기 핸들러"]
H1["모르는 명령 핸들러"]
HD["기본 핸들러"]
H3["타이머 핸들러"]
H4["키보드 핸들러"]
end
V0 --> H0
V1 --> H1
V2 --> HD
V3 --> H3
V4 --> H4
표에 담긴 것은 주소뿐입니다. 핸들러 코드는 표 밖 메모리 어딘가에 따로 놓입니다.
0번과 1번 칸은 장치가 아니라 프로세서 자신이 일으키는 신호의 칸입니다. 1번의 모르는 명령은 프로세서가 해석할 수 없는 명령을 만났다는 뜻입니다. 이런 신호는 아래 「표에 번호를 받는 신호」에서 다룹니다. 2번 칸의 기본 핸들러는 「빈 번호를 맡는 기본 핸들러」에서 다룹니다.
번호로 칸을 찾는 계산
칸의 크기가 모두 같으므로 몇 번째 칸이 메모리 어디에 있는지는 곱셈 한 번으로 나옵니다. 표가 시작하는 주소를 기준 주소라고 부릅니다. 거기에 벡터 번호와 칸 크기를 곱한 값을 더하면 그 칸의 주소입니다.
칸 하나가 4바이트이고 표가 0번지에서 시작한다고 해 봅니다. 3번 신호가 들어오면 프로세서는 이렇게 칸을 찾습니다.
base = 0x0000; // 표가 시작하는 주소
n = 3; // 들어온 벡터 번호
slot = base + n * 4; // 0x000C
프로세서는 0x000C 번지에서 4바이트를 읽습니다. 거기 적힌 값이 타이머 핸들러의 주소입니다. 표를 앞에서부터 훑지 않으므로 칸이 몇 개든 찾는 데 드는 일은 같습니다.
신호 하나가 핸들러에 닿기까지
이 계산과 건너뛰기는 소프트웨어가 아니라 프로세서 하드웨어가 합니다. 운영체제는 표를 미리 채워 두기만 합니다. 신호가 올 때마다 끼어들지 않습니다.
sequenceDiagram
participant 장치
participant 프로세서
participant 표 as 벡터 테이블
participant 핸들러
장치->>프로세서: 3번 신호
Note over 프로세서: 하던 명령의 다음 주소를 저장한다
프로세서->>표: 3번 칸을 읽는다
표-->>프로세서: 타이머 핸들러 주소
프로세서->>핸들러: 그 주소로 건너뛴다
핸들러-->>프로세서: 처리를 마치고 돌아간다
Note over 프로세서: 저장해 둔 주소부터 다시 실행한다
그림의 첫 메모는 돌아올 수 있게 해 주는 단계입니다. 프로세서 안에는 레지스터라는 아주 작은 저장소가 여럿 있습니다. 자주 쓰는 값을 메모리까지 가지 않고 바로 쓰려고 둡니다.
그중 프로그램 카운터는 다음에 실행할 명령의 주소를 들고 있는 레지스터입니다. 프로세서는 이 값이 가리키는 명령을 실행하고 값을 다음 명령으로 넘깁니다.
핸들러로 건너뛰면 프로그램 카운터에는 핸들러 주소가 들어갑니다. 원래 값은 건너뛰기 전에 따로 저장해 둡니다. 저장하는 곳은 프로세서 설계마다 다릅니다. 메모리의 스택에 두기도 하고 그 일만 맡은 레지스터에 두기도 합니다.
핸들러가 할 일을 마치면 돌아가는 전용 명령을 실행합니다. 프로세서는 저장해 둔 주소를 프로그램 카운터에 되돌려 놓고 멈췄던 프로그램을 이어서 돌립니다. 멈췄던 프로그램 쪽에서는 잠깐 쉬었다는 것 말고는 달라진 것이 없습니다.
표에 번호를 받는 신호
지금까지 신호라고 부른 것은 장치가 보내는 인터럽트였습니다. 표에서 번호를 받는 신호는 그것만이 아닙니다. 프로세서가 명령을 실행하다 스스로 멈추는 경우도 같은 표에서 번호를 받습니다. 번호를 받는 신호는 어디서 왔느냐로 세 갈래가 됩니다.
| 갈래 | 누가 일으키나 | 예 |
|---|---|---|
| 인터럽트 | 프로세서 밖의 장치 | 타이머가 울림 · 키보드가 눌림 · 디스크 읽기가 끝남 |
| 예외 | 명령을 실행하던 프로세서 자신 | 0으로 나누기 · 프로세서가 모르는 명령 · 없는 메모리 주소를 읽음 |
| 트랩 | 프로그램이 일부러 | 운영체제에 일을 부탁하는 시스템 콜 |
셋은 들어오는 길만 다릅니다. 프로세서가 번호로 칸을 찾아 건너뛰는 과정은 셋 다 같습니다. 표 하나가 셋을 다 받는 까닭입니다. 흔히 앞쪽 번호를 프로세서 자신의 예외에 주고 뒤쪽 번호를 장치 인터럽트에 줍니다.
전원을 켜면 처음 읽는 칸
많은 작은 프로세서는 표의 첫머리에 전원이 켜졌을 때 갈 곳을 둡니다. 이 칸이 리셋 벡터입니다. 전원이 들어온 프로세서는 아무 코드도 모르는 채로 시작합니다. 어디서부터 실행할지는 이 칸에서 읽습니다.
이런 기계에서는 벡터 테이블이 프로그램 메모리 맨 앞에 놓입니다. 칸을 잘못 채운 프로그램은 켜지는 순간부터 잘못된 주소로 갑니다.
누가 채우고 어디에 두나
표가 언제 채워지는지는 기계마다 다릅니다. 임베디드 기계에서는 표가 펌웨어와 함께 미리 만들어집니다. 펌웨어를 기계에 써 넣을 때 표도 프로그램 메모리 맨 앞에 같이 들어갑니다. 켜진 뒤에 채우는 것이 아니라서 리셋 벡터 칸은 전원이 들어오기 전부터 차 있습니다.
이 프로그램 메모리는 전원이 꺼져도 내용이 남습니다. 대신 실행 중에는 마음대로 고쳐 쓰지 못합니다. 이 성질이 아래 재배치에서 다시 나옵니다.
개인용 컴퓨터나 서버에서는 운영체제의 커널이 부팅하면서 표를 채웁니다. 이때는 켜진 뒤에 도는 코드가 메모리에 표를 만듭니다.
어떤 프로세서는 표를 둘 곳이 하나로 박혀 있습니다. 많은 프로세서는 표의 기준 주소를 담는 레지스터를 따로 둡니다. 그 레지스터에 다른 주소를 적으면 프로세서는 그때부터 새 표를 읽습니다. 이렇게 표를 옮기는 일을 벡터 테이블 재배치라고 부릅니다.
재배치가 필요한 때는 흔히 둘입니다. 첫째는 부트로더가 본 프로그램에 일을 넘겨줄 때입니다. 부트로더는 본 프로그램보다 먼저 돌아 그것을 올려 주는 작은 프로그램입니다. 부트로더는 자기 표로 돌다가 넘겨줄 때 표도 본 프로그램의 것으로 바꿔야 합니다.
둘째는 실행 중에 핸들러를 갈아 끼우려는 때입니다. 처음 표는 실행 중에 고쳐 쓰지 못하는 프로그램 메모리에 있습니다. 이럴 때는 그 표를 RAM(Random Access Memory, 임의 접근 메모리)으로 복사합니다. 그다음 기준 주소를 그쪽으로 돌립니다.
운영체제가 있는 기계에서 일반 프로그램은 이 표를 못 고칩니다. 프로그램이 트랩을 일으켜도 갈 곳은 커널이 적어 둔 칸이 정합니다. 그래서 프로그램이 커널 코드의 아무 데나 뛰어들 수 없습니다. 벡터 테이블은 커널로 들어가는 입구를 몇 개로 못 박아 두는 장치이기도 합니다.
빈 번호를 맡는 기본 핸들러
기계에 달린 장치가 적으면 번호 중 상당수가 쓰이지 않습니다. 그래도 그 칸을 비워 둘 수는 없습니다. 빈 칸에 남은 아무 값이나 주소로 읽히면, 뜻밖의 신호가 왔을 때 프로세서가 아무 곳으로나 건너뜁니다.
쓰지 않는 칸은 전부 기본 핸들러 하나를 가리키게 채웁니다. 기본 핸들러는 오류를 기록하거나 더 진행하지 않고 멈춰 섭니다. 뜻밖의 신호가 와도 기계가 어디서 멈췄는지 알 수 있습니다.
칸에 담는 것
칸에 무엇을 넣는지는 프로세서 설계마다 다릅니다. 널리 쓰이는 방식은 둘입니다.
하나는 지금까지 본 대로 핸들러의 주소를 넣는 방식입니다. 프로세서가 칸에서 주소를 읽어 그리로 건너뜁니다.
다른 하나는 칸에 명령 하나를 넣는 방식입니다. 프로세서는 그 칸으로 곧장 건너가 거기 적힌 명령을 실행합니다. 그 명령이 대개 「핸들러로 건너뛰어라」입니다.
어떤 표는 칸에 주소 말고 더 많은 것을 담습니다. 이 핸들러로 넘어갈 때 어느 권한으로 실행할지 같은 정보를 같이 적습니다. 이렇게 칸이 커진 표의 대표가 인터럽트 디스크립터 테이블입니다.
표 없이 입구 하나로 받는 방식
어떤 프로세서는 벡터 테이블을 안 씁니다. 신호가 무엇이든 프로세서는 늘 같은 주소 하나로 건너뜁니다. 프로세서는 무슨 신호였는지를 레지스터에 번호로 적어 둡니다. 그 입구의 코드가 번호를 읽고 알맞은 핸들러를 골라 부릅니다.
이 방식은 표가 차지할 메모리가 필요 없습니다. 대신 신호마다 번호를 읽고 갈라 보내는 명령을 몇 개 더 실행합니다. 핸들러에 닿기까지 그만큼 더 걸립니다.
신호에 곧바로 응답해야 하는 기계는 벡터 테이블로 그 몇 개의 명령을 덜어 냅니다. 어떤 프로세서는 둘 중 하나를 고르게 해 둡니다.
찾는 비용과 차지하는 공간
자료구조가 얼마나 걸리는지는 빅오 표기법으로 적습니다. O(1) 은 데이터가 늘어도 걸리는 일이 늘지 않는다는 뜻입니다. O(n) 은 데이터 수에 비례해 늘어난다는 뜻입니다.
벡터 테이블은 번호에서 칸의 주소를 계산으로 얻습니다. 칸이 많아져도 찾는 일은 늘지 않습니다.
| 재는 것 | 값 |
|---|---|
| 번호로 핸들러 주소 찾기 | O(1) · 곱셈과 덧셈 한 번, 칸 읽기 한 번 |
| 칸 하나 고치기 | O(1) · 그 칸에 새 주소를 쓴다 |
| 표 전체를 옮기기 | O(1) · 기준 주소 레지스터에 새 주소를 쓴다(새 표를 채우는 일은 별도) |
| 공간 | O(n) · n 은 벡터 번호의 수. 쓰지 않는 번호도 칸을 차지한다 |
공간이 번호 수에 비례한다는 것이 이 표가 치르는 몫입니다. 앞 소절의 입구 하나짜리 방식은 이 공간을 아끼는 대신 찾는 데 명령을 더 씁니다.
이름이 비슷한 가상 함수 테이블
C++ 같은 객체 지향 언어에도 번호로 함수 주소를 찾는 표가 있습니다. 가상 함수를 부를 때 쓰는 가상 함수 테이블입니다. 줄여서 vtable 이라고 부릅니다.
둘은 모양이 닮았습니다. 둘 다 번호로 칸을 찾습니다. 그리고 칸에 든 함수 주소로 건너뜁니다.
하는 일은 다릅니다. 가상 함수 테이블은 프로그램이 객체의 메서드를 부를 때 씁니다. 벡터 테이블은 프로세서가 신호를 받을 때 씁니다.
관련 항목
벡터 테이블을 이루는 구성 요소
벡터 번호 · 인터럽트 벡터 · 리셋 벡터 · 인터럽트 핸들러 · 기본 핸들러 · 기준 주소 레지스터
벡터 테이블에서 번호를 받는 신호
인터럽트 · 예외 · 트랩 · 시스템 콜 · 페이지 폴트 · 마스크 불가능 인터럽트 · 타이머 인터럽트
벡터 테이블을 읽고 채우는 주체
CPU · 레지스터 · 프로그램 카운터 · 펌웨어 · 부트로더 · 커널 · 인터럽트 컨트롤러
같은 일을 다른 모양으로 하는 방식
인터럽트 디스크립터 테이블 · 트랩 벡터 · 폴링 · 점프 테이블 · 함수 포인터
이름이 비슷해 헷갈리는 이웃
가상 함수 테이블 · 벡터 검색 · 벡터 공간 모델 · 벡터 시계
벡터 테이블이 속하는 상위 분류
다른 이름: vector table · interrupt vector table · 인터럽트 벡터 테이블 · 예외 벡터 테이블