시스템 프로그래밍 과목 (2024-1) | C언어 구현 | SIC/XE 아키텍처
이 프로젝트는 교육용 가상 CPU 아키텍처인 SIC/XE를 위한 2-Pass 어셈블러(Assembler) 를 C언어로 직접 구현한 시스템 소프트웨어입니다.
어셈블러는 사람이 읽을 수 있는 어셈블리 언어(Assembly Language)를 CPU가 직접 실행할 수 있는 기계어(Object Code)로 변환해주는 시스템 소프트웨어입니다. 컴파일러나 인터프리터처럼 프로그래밍 언어 처리기의 가장 하위 계층에 해당하며, 하드웨어와 소프트웨어의 경계에서 동작합니다.
SIC(Simplified Instructional Computer) XE(Extra Equipment) 는 Leland Beck의 교재 System Software: An Introduction to Systems Programming에서 소개된 교육 전용 가상 컴퓨터 아키텍처입니다. 실제 CPU(x86, ARM 등)처럼 레지스터, 명령어 형식, 주소지정 방식을 갖추고 있어, 실제 시스템 소프트웨어의 동작 원리를 학습하기에 적합합니다.
주요 레지스터:
| 레지스터 | 번호 | 역할 |
|---|---|---|
| A (Accumulator) | 0 | 산술/논리 연산의 기본 레지스터 |
| X (Index) | 1 | 인덱스 주소지정에 사용 |
| L (Linkage) | 2 | 서브루틴 복귀 주소 저장 |
| B (Base) | 3 | Base 상대 주소지정에 사용 |
| S, T | 4, 5 | 범용 레지스터 |
| PC (Program Counter) | 8 | 다음 실행할 명령어의 주소 |
| SW (Status Word) | 9 | 조건 코드 저장 |
명령어 형식(Instruction Format):
SIC/XE는 명령어 길이에 따라 4가지 형식을 가집니다.
Format 1 (1바이트): [ opcode(8) ]
Format 2 (2바이트): [ opcode(8) | r1(4) | r2(4) ]
Format 3 (3바이트): [ opcode(6) | n | i | x | b | p | e | disp(12) ]
Format 4 (4바이트): [ opcode(6) | n | i | x | b | p | e | address(20) ]
nixbpe 플래그 (Format 3/4의 핵심):
| 비트 | 이름 | 역할 |
|---|---|---|
| n | indirect | 간접 주소지정 (오퍼랜드가 주소의 주소) |
| i | immediate | 즉시 주소지정 (오퍼랜드가 상수값) |
| x | index | 인덱스 레지스터 X를 주소에 더함 |
| b | base | Base 레지스터 기준 상대 주소지정 |
| p | pc-relative | PC 기준 상대 주소지정 |
| e | extended | Format 4 (4바이트 확장) |
어셈블러가 소스코드를 한 번만 읽으면 문제가 생깁니다. 예를 들어, 아직 정의되지 않은 레이블(심볼)을 참조하는 명령어가 있을 때 주소를 알 수 없습니다. 이를 전방 참조(Forward Reference) 문제라 합니다.
2-Pass 방식은 소스코드를 두 번 스캔하여 이 문제를 해결합니다:
[소스코드] → [Pass 1] → [심볼 테이블 / 리터럴 테이블] → [Pass 2] → [오브젝트 코드]
- Pass 1: 소스를 처음부터 끝까지 읽으며 모든 레이블의 메모리 주소를 계산하여 심볼 테이블을 구축합니다.
- Pass 2: 심볼 테이블을 참조하여 각 명령어를 실제 기계어 코드로 변환합니다.
| 과제 | 구현 내용 |
|---|---|
| 과제 1 | Pass 1 (토큰 파싱) + OPCODE 매핑 출력 |
| 과제 2 | Pass 1 (심볼/리터럴 테이블 + LOCCTR) + Pass 2 (오브젝트 코드 전체 생성) |
SIC/XE 어셈블리 소스코드의 각 줄을 파싱하여 라벨(label), 연산자(operator), 피연산자(operand), 주석(comment) 으로 분리하고, 각 연산자에 해당하는 OPCODE(기계어 번호) 를 찾아 출력합니다.
| 함수 | 역할 |
|---|---|
init_inst_table() |
inst_table.txt에서 SIC/XE 명령어 목록을 읽어 테이블 구축 |
init_input() |
어셈블리 소스파일(input.txt)을 줄 단위로 읽기 |
token_parsing() |
한 줄의 소스코드를 탭(tab) 구분자 기준으로 토큰 분리 |
search_opcode() |
연산자 문자열로 inst_table에서 OPCODE 검색 |
assem_pass1() |
전체 소스에 대해 token_parsing 반복 실행 |
make_opcode_output() |
토큰 + OPCODE를 파일로 출력 |
inst_table.txt 읽기
↓
inst_table[] 구축 (명령어명, OPCODE, 형식, 오퍼랜드 수)
↓
input.txt 읽기
↓
각 줄을 token_parsing()으로 파싱 → tokens[] 구축
↓
각 token의 operator로 search_opcode() 호출
↓
output_20203099.txt에 [레이블 | 연산자 | 피연산자 | OPCODE] 출력
소스코드는 탭 문자(\t)를 구분자로 사용합니다:
[레이블]\t[연산자]\t[피연산자]\t[주석]
- 줄 첫 글자가
.이면 전체 주석 처리 - 줄 첫 글자가
\t이면 레이블 없음 - 피연산자는
,로 구분하여 최대 3개까지 파싱
입력 (input.txt):
COPY START 0 COPY FILE FROM IN TO OUTPUT
FIRST STL RETADR SAVE RETURN ADDRESS
CLOOP +JSUB RDREC READ INPUT RECORD
LDA LENGTH TEST FOR EOF
COMP #0
출력 (output_20203099.txt):
COPY START 0
FIRST STL RETADR 14
CLOOP +JSUB RDREC 48
LDA LENGTH 00
COMP #0 28
각 행 마지막 컬럼이 해당 명령어의 16진수 OPCODE입니다. +JSUB처럼 + 접두사(Format 4)가 붙은 경우에도 접두사를 제거하고 JSUB로 검색합니다.
과제 1의 Pass 1을 확장하여 심볼 테이블, 리터럴 테이블, 위치 계산기(LOCCTR) 를 완성하고, Pass 2에서 SIC/XE 표준 오브젝트 코드를 생성합니다. 생성된 오브젝트 코드는 링커(Linker)와 로더(Loader)가 프로그램을 메모리에 올리는 데 사용됩니다.
/* 명령어 하나의 정보 */
typedef struct _inst {
char str[10]; // 명령어 이름 (예: "LDA", "JSUB")
unsigned char op; // OPCODE 바이트 (예: 0x00, 0x48)
int format; // 명령어 형식 (1, 2, 3)
int ops; // 오퍼랜드 개수
} inst;
/* 소스코드 한 줄을 분해한 토큰 */
typedef struct _token {
char *label; // 레이블
char *operator; // 연산자
char *operand[MAX_OPERAND_PER_INST]; // 피연산자 (최대 3개)
char *comment; // 주석
char nixbpe; // 주소지정 비트 플래그
} token;
/* 심볼 테이블의 항목 */
typedef struct _symbol {
char name[10]; // 심볼(레이블) 이름
int addr; // 메모리 주소
char section[10]; // 소속 제어 섹션
} symbol;
/* 리터럴 테이블의 항목 */
typedef struct _literal {
char literal[20]; // 리터럴 표현식 (예: "=C'EOF'", "=X'05'")
int addr; // 메모리 주소
} literal;LOCCTR(Location Counter): 현재 처리 중인 명령어가 메모리의 어느 주소에 위치할지 추적하는 변수입니다.
처리하는 어셈블러 지시어:
| 지시어 | 역할 | LOCCTR 변화 |
|---|---|---|
START |
프로그램 시작, 초기 주소 설정 | 초기값으로 설정 |
CSECT |
새 제어 섹션 시작 | 0으로 리셋 |
RESW n |
n 워드(3n 바이트) 메모리 예약 | +3n |
RESB n |
n 바이트 메모리 예약 | +n |
WORD |
3바이트 정수 상수 | +3 |
BYTE |
1바이트 이상 상수 (C'', X'') |
+문자열 길이 |
EQU |
심볼에 표현식 값 할당 | 변화 없음 |
LTORG |
현재 위치에 리터럴 풀 배치 | +리터럴 크기 합 |
EXTDEF |
외부에 공개할 심볼 선언 | 변화 없음 |
EXTREF |
외부에서 가져올 심볼 선언 | 변화 없음 |
EQU 처리 (심볼 간 차이 계산):
// EQU BUFEND-BUFFER 처리 예시
// 두 심볼의 주소 차이를 계산하여 새 심볼에 할당
addr = symbol_table["BUFEND"].addr - symbol_table["BUFFER"].addr;Pass 1에서 구축된 심볼/리터럴 테이블을 기반으로 각 명령어를 실제 기계어 바이트로 변환합니다.
Format별 코드 생성:
Format 1: [ opcode(8) ]
예) FIX → C4
Format 2: [ opcode(8) | r1(4) | r2(4) ]
예) COMPR A,S → A0 04
Format 3: [ opcode(6)|n|i | x|b|p|e | disp(12) ]
예) LDA LENGTH → 00 (n=1,i=1,p=1) disp = LENGTH주소 - (PC+3)
Format 4: [ opcode(6)|n|i | x|b|p|e | address(20) ]
예) +JSUB RDREC → 4B (e=1) address = 0 (링킹 시 수정)
주소지정 모드 결정 로직:
// '#' 접두사 → 즉시 주소지정 (n=0, i=1)
// '@' 접두사 → 간접 주소지정 (n=1, i=0)
// 기본 → 단순/직접 (n=1, i=1)
// ',X' 접미사 → 인덱스 주소지정 (x=1)
// PC 상대 주소지정 (3바이트 명령어 기본값)
disp = target_addr - (locctr + 3); // -2048 ~ +2047 범위이면 p=1생성되는 오브젝트 코드는 링커가 처리하는 표준 레코드 형식으로 출력됩니다:
| 레코드 | 형식 | 의미 |
|---|---|---|
| H | H섹션명 시작주소 섹션길이 |
Header: 섹션 기본 정보 |
| D | D심볼명 주소 ... |
Define: 외부에 공개하는 심볼 목록 |
| R | R심볼명 ... |
Reference: 외부에서 참조하는 심볼 목록 |
| T | T시작주소 길이 코드 |
Text: 실제 기계어 코드 (최대 30바이트/레코드) |
| M | M주소 길이 +심볼 |
Modification: 링킹 시 수정이 필요한 위치 |
| E | E실행주소 |
End: 프로그램 진입점 |
수정 레코드(M Record)의 역할:
Format 4 명령어에서 외부 섹션의 심볼을 참조할 때, 링킹 전에는 주소를 알 수 없습니다. M 레코드는 링커에게 "이 주소의 값을 해당 심볼의 실제 주소로 수정하라"고 알립니다.
| 파일 | 설명 |
|---|---|
inst_table.txt |
SIC/XE 명령어 목록 (이름, 형식, OPCODE, 오퍼랜드 수) |
input.txt |
어셈블할 SIC/XE 소스코드 |
inst_table.txt 형식:
LDA,3,00,1
JSUB,3,48,1
CLEAR,2,B4,1
FIX,1,C4,0
(명령어명, 형식(바이트수), OPCODE(16진수), 오퍼랜드 개수)
과제 1:
| 파일 | 설명 |
|---|---|
output_20203099.txt |
소스코드 + 각 명령어의 OPCODE |
과제 2:
| 파일 | 설명 |
|---|---|
output_symtab.txt |
심볼 테이블 (심볼명, 주소, 소속 섹션) |
output_littab.txt |
리터럴 테이블 (리터럴 표현식, 주소) |
output_objectcode.txt |
SIC/XE 오브젝트 코드 (H/D/R/T/M/E 레코드) |
심볼 테이블 (output_symtab.txt):
COPY 0
FIRST 0 +1 COPY
CLOOP 3 +1 COPY
BUFFER 33 +1 COPY
BUFEND 1033 +1 COPY
RDREC 0
RLOOP 9 +1 RDREC
리터럴 테이블 (output_littab.txt):
=C'EOF' 30
=X'05' 1B
오브젝트 코드 (output_objectcode.txt):
HCOPY 000000001033
DBUFFER000033BUFEND001033LENGTH00002D
RRDREC WRREC
T0000001D1720274B1000000320232900003320074B1000003F2FEC0320160F2016
T00001D0D0100030F200A4B1000003E2000
T00003003454F46
M00000405+RDREC
M00001105+WRREC
M00002405+WRREC
E000000
HRDREC 00000000002B
...
이 프로젝트의 입력 파일은 세 개의 제어 섹션으로 구성된 멀티-섹션 프로그램입니다:
COPY ─ 메인 섹션: 파일을 읽어 출력하는 루프 제어
RDREC ─ 서브루틴: 입력 장치에서 레코드를 버퍼에 읽음
WRREC ─ 서브루틴: 버퍼의 내용을 출력 장치에 씀
각 섹션은 독립적으로 어셈블되며, EXTDEF/EXTREF로 섹션 간 심볼을 공유합니다. 링커는 M(Modification) 레코드를 참고하여 외부 심볼 참조를 실제 주소로 연결합니다.
시스템프로그래밍24/
├── 과제1/
│ ├── my_assembler_20203099.c # Pass 1 + OPCODE 출력 구현
│ ├── my_assembler_20203099.h # 자료구조 및 함수 선언
│ ├── inst_table.txt # SIC/XE 명령어 목록
│ ├── input.txt # 어셈블리 소스코드 (3-섹션 프로그램)
│ └── output_20203099.txt # 출력: OPCODE 매핑 결과
│
└── 과제2/
├── my_assembler_20203099-1.c # 완전한 2-Pass 어셈블러 구현
├── my_assembler_20203099-1.h # 자료구조 및 함수 선언
├── inst_table.txt # SIC/XE 명령어 목록
├── input.txt # 어셈블리 소스코드 (동일 3-섹션 프로그램)
├── output_symtab.txt # 출력: 심볼 테이블
├── output_littab.txt # 출력: 리터럴 테이블
└── output_objectcode.txt # 출력: SIC/XE 오브젝트 코드
- 언어: C (표준 C99)
- 빌드 환경: GCC / CLion (CMake)
구현 난이도 및 학습 포인트:
- 문자열 파싱: 탭 구분자 기반 토큰 파싱, 문자열 포인터 범위(
[start, end)) 기반 동적 메모리 할당 - 비트 연산: nixbpe 6개 비트 플래그를 비트 마스킹으로 조합하여 명령어 코드 생성
- PC 상대 주소 계산:
disp = target - (locctr + 3)및 범위 초과 시 Base 상대로 전환 - EQU 심볼 처리: 심볼 테이블을 역참조하여 표현식(
BUFEND-BUFFER) 계산 - LTORG/END 리터럴 배치: 아직 주소가 할당되지 않은 리터럴에 현재 LOCCTR 기준 주소 부여
- 제어 섹션 분리: 여러 섹션을 독립적으로 추적하며 H/D/R/T/M/E 레코드 각각 생성
- 수정 레코드 생성: Format 4 외부 참조 명령어 발견 시 자동으로 M 레코드 추가
# 과제 1 실행 (inst_table.txt, input.txt가 같은 디렉터리에 있어야 함)
cd 과제1
gcc my_assembler_20203099.c -o assembler
./assembler
# → output_20203099.txt 생성
# 과제 2 실행
cd 과제2
gcc my_assembler_20203099-1.c -o assembler2
./assembler2
# → output_symtab.txt, output_littab.txt, output_objectcode.txt 생성- Leland L. Beck, System Software: An Introduction to Systems Programming, 3rd Edition
- SIC/XE Architecture Specification (교재 부록)