배경 설명
이번 학기 컴퓨터 구조를 수강 중이다. 이번 과제는 16진수를 어셈블리 코드(Assembly code)로 바꾸는 것이다. 어셈블리 코드는 컴퓨터가 이해할 수 있는 기계어를 사람이 읽고 쓰기 쉽게 바꾼 언어이다. 예를 들어, 이진수 00000001010110101011(그냥 예시이다)라는 명령어는 사람이 읽기 무척 어렵지만, add x9 x10 x9와 같은 어셈블리 코드로 바꾸면 우리가 이해하기 훨씬 수월해진다. 즉, 어셈블리 코드는 기계어와 1:1로 대응되는, 사람이 읽고 쓰기 쉬운 저수준 언어다.
16진수를 2진수로, 2진수를 RISC-V 어셈블리 코드로 바꿔보자!
- 문제 풀이의 조건
- c나 cpp로 구현할 것!
- input.txt에서 받은 값을 쓴다. input.txt에는 테스트케이스 수와 테스트케이스들이 한 줄에 하나씩 차례로 적혀있다.
- 어셈블리 코드로 바꾼 결과는 한 줄씩 output.txt에 출력한다. (형식 예: add x9,x21,x9)
- 예외 상황은 없다고 가정하고 따로 예외처리를 하지 않을 예정이다. (하면 더 좋겠지만 교수님이 예쁜 input 파일만 준다고 믿어보겠다...)
- 들어가기에 앞서
- 내가 생각했을 때 이 과제에서 중요한 것은 두 가지이다.
- 16진수를 2진수로 바꾸는 것
- 2진수를 어셈블리어로 바꾸는 것
- 2번을 위해서 RISC-V 명령어의 구조를 이해하기
- RISC-V 명령어에 대해서 알아보자!

RISC-V 타입 별 비트 필드 배치 - 명령어가 32비트로 고정되어 있다.
- 각 명령어 타입(R, I, S 등이 있다)에 따라 비트 필드의 의미가 달라진다. 더 쉽게 말하자면, 어떤 타입이냐에 따라서 32비트 중 어떤 부분이 무엇을 말하는지가 달라진다. (약간, 영어랑 한국어의 주어 목적어 서술어 순서가 다른 것과 비슷하다. 분명 같은 요소들이 한 문장에 있지만 그 순서는 타입에 따라 다르다.) 즉, 각 타입별로 rd, rs1, rs2, funct3, funct7, immediate 등의 필드가 다르게 배치된다!
- 비트 31번이 MSB이고 0번이 LSB이다.
- opcode(명령어 타입을 알려줌)는 비트 0~6에 위치한다. 즉, 가장 끝 7자리가 opcode이다!
- 내가 생각했을 때 이 과제에서 중요한 것은 두 가지이다.
- 문제 풀이
- 16진수를 2진수로 변환하기
- 16진수 한 자리는 2진수 4자리와 1:1로 대응된다.

2진수와 대응되는 16진수 - 문제를 풀기위해, 16진수 -> 2진수 변환표를 미리 준비해두자. (나는 상수 배열로 선언했다.)
- 주어진 16진수 문자열을 한 글자씩 읽어 미리 준비한 변환 테이블에서 2진수 문자열로 바꾼다.
- 만약 입력값이 여덟자리보다 짧으면 앞쪽을 0으로 채워줘야 한다. 참고로, 그래도 값은 똑같다. 이걸 패딩한다고 한다.
- 16진수 한 자리는 2진수 4자리와 1:1로 대응된다.
- 2진수를 어셈블리 코드로 변환하기
- 핵심은, 명령어 타입에 맞게 각 필드를 추출하는 것! (참고로, 명령어 타입 별 필드 배치는 외울 필요 없다. 그냥 찾아서 하면 된다. 교수님이 외우지 말라고 5번은 말한 듯)

이렇게 명령어에 맞게 필드를 추출해야 한다. - opcode와 funct3, funct7으로 명령어 타입을 판별한다. switch문이나 if문을 이용하여 경우를 나누고, 문자열 인덱스를 조정해 각 필드를 추출해보자. (나는 if문으로 했다. 그러나 조건이 많아진다면 아마 switch문 더 편할 것이다.)
- 핵심은, 명령어 타입에 맞게 각 필드를 추출하는 것! (참고로, 명령어 타입 별 필드 배치는 외울 필요 없다. 그냥 찾아서 하면 된다. 교수님이 외우지 말라고 5번은 말한 듯)
- 어셈블리 코드 출력하기!
- 16진수를 2진수로 변환하기
- 핵심 로직 코드
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
// 16진수 한 자리(인덱스의 값)와 대응되는 4비트 이진 문자열 표
const char *BIN_TABLE[16] = {
"0000","0001","0010","0011",
"0100","0101","0110","0111",
"1000","1001","1010","1011",
"1100","1101","1110","1111"
};
// 주어진 이진 문자열(binstr)에서 start 위치부터 len 만큼의 비트를 잘라 정수로 변환하는 함수(자주 쓰기 때문에 모듈화)
int get_bits(const char *binstr, int start, int len) {
char temp[33] = {0};
strncpy(temp, binstr + start, len);
temp[len] = '\0';
return strtol(temp, NULL, 2);
}
int main(void) {
FILE *fin = fopen("input.txt", "r");
FILE *fout = fopen("output.txt", "w");
char line[256];
fgets(line, sizeof(line), fin);
int T = atoi(line); // 테스트 케이스 개수 읽기
// 16진수 한 줄씩 읽기
for (int tc = 0; tc < T; tc++) {
fgets(line, sizeof(line), fin);
// 이진수로 바꾸기에 앞서 0x 제거하기
char *s = line;
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2;
// 16진수 → 이진수 변환
char binstr[129] = {0};
int pos = 0;
// 줄 끝까지 반복
for (; *s && *s != '\n' && *s != '\r'; s++) {
// 소문자면 대문자로 변환
char c = (char)toupper((unsigned char)*s);
// 16진수 인덱스로 변환
int idx;
if (c >= '0' && c <= '9') {
idx = c - '0';
} else {
idx = c - 'A' + 10;
}
strncpy(binstr+pos, BIN_TABLE[idx], 4);
pos += 4;
}
binstr[pos] = '\0';
// 총 길이가 32비트보다 짧으면 앞을 0으로 채워주기
int binlen = strlen(binstr);
char bin32[33];
if (binlen < 32) {
int pad = 32 - binlen;
for (int i = 0; i < pad; i++)
bin32[i] = '0';
strcpy(bin32 + pad, binstr);
} else {
strncpy(bin32, binstr, 32);
}
bin32[32] = '\0';
// opcode 추출(마지막 7비트가 opcode임)
char opcode[8] = {0};
strncpy(opcode, bin32 + 25, 7);
opcode[7] = '\0';
char asm_line[128] = {0};
// ===== 명령어 변환 =====
if (strcmp(opcode, "0000011") == 0) {
// I-Type: lw
int rd = get_bits(bin32, 20, 5);
int funct3 = get_bits(bin32, 17, 3);
int rs1 = get_bits(bin32, 12, 5);
int imm = get_bits(bin32, 0, 12);
sprintf(asm_line, "lw x%d,%d(x%d)", rd, imm, rs1);
} else if (strcmp(opcode, "0100011") == 0) {
// S-Type: sw
// immediate 값을 두 부분에서 받으니 주의
int imm_hi = get_bits(bin32, 0, 7);
int rs2 = get_bits(bin32, 7, 5);
int rs1 = get_bits(bin32, 12, 5);
int funct3 = get_bits(bin32, 17, 3);
int imm_lo = get_bits(bin32, 20, 5);
int imm = (imm_hi << 5) | imm_lo;
sprintf(asm_line, "sw x%d,%d(x%d)", rs2, imm, rs1);
} else if (strcmp(opcode, "0010011") == 0) {
// I-Type: addi
int rd = get_bits(bin32, 20, 5);
int funct3 = get_bits(bin32, 17, 3);
int rs1 = get_bits(bin32, 12, 5);
int imm = get_bits(bin32, 0, 12);
sprintf(asm_line, "addi x%d,x%d,%d", rd, rs1, imm);
} else if (strcmp(opcode, "0110011") == 0) {
// R-Type: add/sub/and
int rd = get_bits(bin32, 20, 5);
int funct3 = get_bits(bin32, 17, 3);
int rs1 = get_bits(bin32, 12, 5);
int rs2 = get_bits(bin32, 7, 5);
int funct7 = get_bits(bin32, 0, 7);
// R-Type은 funct3, funct7 조합으로 명령어 결정함
if (funct3 == 0) {
if (funct7 == 0)
sprintf(asm_line, "add x%d,x%d,x%d", rd, rs1, rs2);
else if (funct7 == 32)
sprintf(asm_line, "sub x%d,x%d,x%d", rd, rs1, rs2);
} else if (funct3 == 7 && funct7 == 0) {
sprintf(asm_line, "and x%d,x%d,x%d", rd, rs1, rs2);
}
} else if (strcmp(opcode, "1100011") == 0) {
// B-Type: beq
// immediate 값을 여러 부분에서 받으니 주의
int imm_12 = get_bits(bin32, 0, 1);
int imm_10_5 = get_bits(bin32, 1, 6);
int rs2 = get_bits(bin32, 20, 5);
int rs1 = get_bits(bin32, 12, 5);
int funct3 = get_bits(bin32, 17, 3);
int imm_4_1 = get_bits(bin32, 7, 4);
int imm_11 = get_bits(bin32, 11, 1);
// 비트 위치 재배열
int imm = (imm_12 << 12) | (imm_11 << 11) | (imm_10_5 << 5) | (imm_4_1 << 1);
if (funct3 == 0)
sprintf(asm_line, "beq x%d,x%d,%d", rs1, rs2, imm);
}
fprintf(fout, "%s\n", asm_line); // 결과 파일에 출력
}
fclose(fin);
fclose(fout);
return 0;
}
간단한 회고
로직이 무척 어렵지는 않으나 C코드가 어렵다...
16진수를 2진수로, 2진수를 어셈블리어로 바꾸는 게 포인트다.
어셈블리어로 바꿀 때는 명령어 타입에 맞게 필드를 잘 추출해야 한다. (숫자 잘못 세서 인덱스를 잘못 적었더니 처음에 값이 다르게 나와서 뭐가 문제지 했다)
'공부중' 카테고리의 다른 글
| [소셜벤처창업] 온마을 프로젝트 회고록 (0) | 2026.06.24 |
|---|---|
| [자료구조] 스택 관련 문제 풀이(과제 1) (0) | 2025.09.29 |