안녕하세요?
지난 시간에 이어 예제를 통한 C언어 기초 강의 2편을 시작해 보겠습니다.
1편은 아래 링크 참고 바랍니다.
https://cpro95.tistory.com/118
그럼 1편에서의 코드를 보고 이어서 진행해 보겠습니다.
#include "args.h"
char* helptext =
"Usage: hexview [file]\n"
"\n"
"Arguments:\n"
" [file] File to read (default: STDIN).\n"
"\n"
"Options:\n"
" -l, --line <int> Bytes per line in output (default: 16).\n"
" -n, --num <int> Number of bytes to read (default: all).\n"
" -o, --offset <int> Byte offset at which to begin reading.\n"
"\n"
"Flags:\n"
" -h, --help Display this help text and exit.\n"
" -v, --version Display the version number and exit.\n";
int main(int argc, char** argv) {
// 새로운 ArgParser 인스턴스를 만듭니다.
ArgParser* parser = ap_new();
ap_helptext(parser, helptext);
ap_version(parser, "0.1.0");
// 커맨드라인 아규먼트를 파싱합니다.
ap_parse(parser, argc, argv);
ap_free(parser);
}
실행 결과는 아래 그림과 같습니다.
일단 ArgParser 사용법에 대해 알아봅시다.
다음 코드를 보십시오.
#include <stdio.h>
#include "args.h"
char* helptext =
"Usage: hexview [file]\n"
"\n"
"Arguments:\n"
" [file] File to read (default: STDIN).\n"
"\n"
"Options:\n"
" -l, --line <int> Bytes per line in output (default: 16).\n"
" -n, --num <int> Number of bytes to read (default: all).\n"
" -o, --offset <int> Byte offset at which to begin reading.\n"
"\n"
"Flags:\n"
" -h, --help Display this help text and exit.\n"
" -v, --version Display the version number and exit.\n";
int main(int argc, char** argv) {
// 새로운 ArgParser 인스턴스를 만듭니다.
ArgParser* parser = ap_new();
ap_helptext(parser, helptext);
ap_version(parser, "0.1.0");
// 파서(parser)에 새로운 옵션을 디폴트 값과 함께 지정합니다.
// --line or -l ==> 디폴트 값은 16 ==> 한줄에 몇개씩 출력할지 알려주는 line 값
// --num or -n ==> 디폴트 값은 -1 ==> 읽어들일 byte 값
// --offset or -o ==> 디폴트 값은 0 ==> 파일의 특정 위치 값
ap_int_opt(parser, "line l", 16);
ap_int_opt(parser, "num n", -1);
ap_int_opt(parser, "offset o", 0);
// 커맨드라인 아규먼트를 파싱합니다.
ap_parse(parser, argc, argv);
// 옵션을 읽어 들입니다.
int line_length = ap_int_value(parser, "line");
int offset = ap_int_value(parser, "offset");
int bytes_to_read = ap_int_value(parser, "num");
printf("line_length : %d\n", line_length);
printf("offset : %d\n", offset);
printf("bytes_to_read : %d\n", bytes_to_read);
// 옵션이 아닌 아규먼트를 읽습니다.
// $hexview arg1 arg2 arg3
// ap_arg(parser, 0) ==> arg1에 해당됩니다.
// ap_arg(parser, 1) ==> arg2에 해당됩니다.
// ap_arg(parser, 2) ==> arg3에 해당됩니다.
char* arg1 = ap_arg(parser,0);
char* arg2 = ap_arg(parser,1);
char* arg3 = ap_arg(parser,2);
printf("arg1 : %s\n", arg1);
printf("arg2 : %s\n", arg2);
printf("arg3 : %s\n", arg3);
// 파서(parser)를 해제합니다.
ap_free(parser);
}
코드 주석을 보시면 ArgParser 사용법에 대해 쉽게 이해하실 수 있으실 겁니다.
우리가 많이 이용할 함수는 ap_int_value() 즉, 옵션 값을 int 변수로 가지고 오는 겁니다.
두 번째 함수는 ap_arg() 함수로 옵션이 아닌 아규먼트를 불러올 수 있습니다. 0부터 시작합니다.
실행 결과를 한번 보겠습니다.
아주 잘 실행되고 있습니다.
그럼 본격적으로 파일의 hexview를 보기 위한 FILE IO 및 더 깊은 코드로 들어가 보겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h> // bool, true, false가 정의된 헤더파일
#include <stdint.h> // 시스템에 상관없이 명확한 int값 표현을 위해 사용.
#include "args.h"
char* helptext =
"Usage: hexview [file]\n"
"\n"
"Arguments:\n"
" [file] File to read (default: STDIN).\n"
"\n"
"Options:\n"
" -l, --line <int> Bytes per line in output (default: 16).\n"
" -n, --num <int> Number of bytes to read (default: all).\n"
" -o, --offset <int> Byte offset at which to begin reading.\n"
"\n"
"Flags:\n"
" -h, --help Display this help text and exit.\n"
" -v, --version Display the version number and exit.\n";
int main(int argc, char** argv) {
// 새로운 ArgParser 인스턴스를 만듭니다.
ArgParser* parser = ap_new();
ap_helptext(parser, helptext);
ap_version(parser, "0.1.0");
// 파서(parser)에 새로운 옵션을 디폴트 값과 함께 지정합니다.
// --line or -l ==> 디폴트 값은 16 ==> 한줄에 몇개씩 출력할지 알려주는 line 값
// --num or -n ==> 디폴트 값은 -1 ==> 읽어들일 byte 값
// --offset or -o ==> 디폴트 값은 0 ==> 파일의 특정 위치 값
ap_int_opt(parser, "line l", 16);
ap_int_opt(parser, "num n", -1);
ap_int_opt(parser, "offset o", 0);
// 커맨드라인 아규먼트를 파싱합니다.
ap_parse(parser, argc, argv);
// 읽어들일 파일을 지정하지 않으면
// 즉, 아규먼트가 없으면 기본으로 stdin을 FILE 구조체로 정합니다.
FILE* file = stdin;
// ap_has_args() 함수는
// 옵션말고 아규먼트가 있는지 체크하는 함수입니다.
// 예을 들어 ./bin/hexview arg1 -n 11 ==> 여기서 arg1 에 해당
// true / false 를 리턴합니다.
// 읽어들일 파일을 지정했으면 (아규먼트가 있으면)
if (ap_has_args(parser)) {
// 그 아규먼트를 filename으로 지정하고
char* filename = ap_arg(parser, 0);
// 그 파일을 오픈합니다. "read" "binary" 옵션을 줘서
// 우리의 목적에 맞게 바이너리를 읽어 들입니다.
file = fopen(filename, "rb");
// 파일 오픈시 에러났을 때 에러 출력도 해줍니다.
if (file == NULL) {
fprintf(stderr, "Error: cannot open the file '%s'.\n", filename);
exit(1);
}
}
// Try seeking to the specified offset.
// 특정 오프셋으로 이동하는 역할을 합니다.
// 먼저 offset 옵션을 int offset 변수에 저장하고
int offset = ap_int_value(parser, "offset");
// offset 변수가 0이 아니면
if (offset != 0) {
// fseek 함수 설명입니다.
// fopen으로 FILE 객체(구조체)를 file이라는 이름으로 오픈하고
// 그다음 fseek함수를 사용해 file에서 위치를 이동 시킵니다.
// offset은 맨 마지막 변수인 origin 에서 얼마나 떨어진 곳으로 설정할지 나타내는 변수
// SEEK_SET은 origin 상수로써 offset이 더해지는 위치입니다.
// SEEK_SET ==> 파일의 시작
// SEEK_CUR ==> 현재 파일 포인터의 위치
// SEEK_END ==> 파일의 끝
// 즉 아래 코드는 file 객체에서 offset 변수 만큼 파일의 시작(SEEK_SET)에서
// 이동한다는 얘기입니다.
// 에러가 나면 0이 아닌 값을 리턴합니다.
if (fseek(file, offset, SEEK_SET) != 0) {
// fseek 함수 에러 처리시
fprintf(stderr, "Error: cannot seek to the specified offset.\n");
exit(1);
}
}
// num 옵션 값 : 읽어들일 바이트 값
int bytes_to_read = ap_int_value(parser, "num");
// line 옵션 값 : 한줄에 몇개의 hex를 나타낼지 지정하는 값
int line_length = ap_int_value(parser, "line");
// 옵션에 따라 hexview를 보여주는 함수
// 일단 컴파일을 위해 아래 코드는 주석처리합니다.
// dump_file(file, offset, bytes_to_read, line_length);
//열었던 file을 닫습니다.
fclose(file);
// 파서(parser)를 해제합니다.
ap_free(parser);
}
코드 설명은 주석을 보시면 쉽게 이해할 수 있습니다.
hexview 실행을 위해 아규먼트와 옵션을 알맞은 변수에 저장한 후 hexview를 출력 하는 dump_file을 호출하는 코드입니다.
이제 본격적으로 dump_file 함수를 알아보겠습니다.
void dump_file(FILE* file, int offset, int bytes_to_read, int line_length) {
// stdint.h 헤더에서 정의한 unsigned int 8비트 정수라고 명확하게 선언합니다.
// buffer는 한줄에 몇개의 hex를 보여줄 지 메모리 할당을 합니다.
// --line 옵션이 디폴트 값이 16이라서
// unsigned int 8bit x 16개의 메모리 블록을 할당하게 됩니다.
uint8_t* buffer = (uint8_t*)malloc(line_length);
// 버퍼가 NULL일 때, 즉 위에서 malloc 함수가 잘못 되었을 때
// 에러 코드 출력
if (buffer == NULL) {
fprintf(stderr, "Error: insufficient memory.\n");
exit(1);
}
// 무한 루프를 돌립니다.
while (true) {
// max_bytes 라는 변수를 만들고
int max_bytes;
// bytes_to_read라고 --num 옵션값입니다.
// 즉, 얼마나 읽어 들이냐 입니다.
// 디폴트는 -1로 세팅되어 있어서 디폴트일 경우 전부다 읽어들이라는 뜻입니다.
// --num 옵션값이 0보다 작을 때, 즉 디폴트 값일 때
if (bytes_to_read < 0) {
// max_bytes 에 line_length 즉, 한줄에 몇 개의 hex를 표현하는 값을 지정합니다.
// line_length 는 --line 옵션으로 디폴트 값이 16입니다.
max_bytes = line_length;
} else if (line_length < bytes_to_read) { // 만약 읽어들일 바이트가 --line 옵션보다 크면
// max_bytes 는 --line 옵션으로 세팅하고
max_bytes = line_length;
} else {
// 그렇지 않으면 max_bytes는 읽어들일 값으로 세팅합니다.
max_bytes = bytes_to_read;
}
// fread 를 통해 file 객체에서 데이터를 읽어들입니다.
// 아래 코드를 보시면
// file 객체에서 max_bytes 만큼 읽어서 buffer에 저장합니다.
// 읽어들일 각각의 값은 sizeof(uint8_t) 크기로 정했습니다.
// fread 가 성공적으로 지정한 원소의 개수 만큼 읽어들였다면 읽어들인 원소의 개수가
// size_t 타입으로 리턴됩니다.
size_t num_bytes = fread(buffer, sizeof(uint8_t), max_bytes, file);
// 읽어들인 원소의 개수가 0보다 클때, 즉, 에러가 없을 때
if (num_bytes > 0) {
// print_line이라는 별도의 출력 함수에
// buffer와 읽어들인 원소의 개수(num_bytes)와
// 읽어들인 offset 위치,
// 한줄에 몇개씩 표시하는지의 line_length 값을 전달합니다.
// 한줄 한줄 출력하는 로직입니다.
print_line(buffer, num_bytes, offset, line_length);
offset += num_bytes;
bytes_to_read -= num_bytes;
} else {
// fread 리턴값이 읽어들인 원소가 없을 때
// while(true) 의 무한 루프를 탈출합니다.
break;
}
}
// buffer를 해제합니다.
free(buffer);
}
메모리 할당 malloc과 fread를 통해 파일에서 데이터를 읽어 들인 후 print_line 함수로 출력하는 코드입니다.
주석에 자세히 코드 설명이 되어 있으니 참조 바랍니다.
다음은 이제 읽어 들인 파일의 hex값을 출력하는 print_line에 대해 알아보겠습니다.
print_line에서는 C 스탠더드 IO 함수인 printf를 사용할 예정입니다.
먼저 printf의 출력 형식에 대해 잠깐 알아보겠습니다.
printf("% d", int_value); 형식으로 사용되는데 각각의 데이터 타입에 대한 인수가 아래 표에 있으니 참고 바랍니다.
type 인수 |
설명 |
d |
int값을 부호있는 10진수로 출력 |
i |
d와 같음 |
u |
int값을 부호없는 10진수로 출력 |
X |
int값을 부호없는 16진수로 출력 10~15은 'A'~'F'로 표시 |
x |
int값을 부호없는 16진수로 출력 10~15은 'a'~'f'로 표시 |
o |
int값을 부호없는 8진수로 출력 |
p |
포인터값을 16진수로 출력 |
s |
문자열 출력 |
c |
int값을 문자로 출력 |
C |
c와 같음 |
f |
double값을 소수로 출력 (예:12.566371) |
e |
double값을 지수로 출력 (예:1.256637e+001) |
E |
e와 같음 'e'가 'E'로 표시 (예:1.256637E+001)。 |
g |
숫자값의 크기에 따라 f나 e로 출력 (예:12.5664、2.99792e+008) 숫자값의 절대치가 너무 커서 precision의 자리수를 넘는 경우와 숫자값의 절대값이 0.0001보다 작은 경우 e형식이 사용되어짐. 그 외의 경우는 f형식으로 사용됨 |
G |
9와 같음 'e'가 'E'로 표시 |
이제 print_line 코드를 보겠습니다.
void print_line(uint8_t* buffer, size_t num_bytes, int offset, int line_length) {
// 읽어 들이는 현재 위치 offset을 출력합니다.
// 8칸으로 지정했으며, int값을 부호없는 16진수로 출력 10~15은 'A'~'F'로 표시합니다.
printf("%8X |", offset);
// 루프입니다.
// 한줄에 표시하는 개수만큼 루프를 돌립니다.
// line_length 의 디폴트 값은 16입니다.
for (int i = 0; i < line_length; i++) {
// 8개 출력했을 때 한깐 띄워서 보기 좋게 합니다.
if (i > 0 && i % 8 == 0) {
printf(" ");
}
// 한줄에 나타날 num_bytes 보다 작으면
// buffer에서 해당되는 i 인덱스 값으로
// 해당 값을 2자리로 16진수 형태로 출력합니다.
if (i < num_bytes) {
printf(" %02X", buffer[i]);
} else {
// num_bytes보다 크거나 같은 때는 빈칸 출력
printf(" ");
}
}
// 여기서 부터는 16진수가 아닌 텍스트 형태로 출력하는 코드입니다.
printf(" | ");
// for 루프를 돌립니다.
for (int i = 0; i < num_bytes; i++) {
// 출력할 buffer[i]값이 보일수 있는 영어 같은 값이면
if (buffer[i] > 31 && buffer[i] < 127) {
// 해당 buffer[i] 값을 c 형태 즉, int값을 문자로 출력합니다.
printf("%c", buffer[i]);
} else {
// 영어로 표현할 수 없는 데이터일경우 . 으로 표시합니다.
printf(".");
}
}
// 한줄이 끝났기 때문에 다음줄로 넘깁니다.
printf("\n");
}
printf 함수 설명을 참고하시고 주석을 잘 읽어 보시면 이해가 되실 겁니다.
이제 모든 코드가 완성되었습니다.
이제 실행해 볼까요?
아주 잘 실행되는군요.
이제 우리의 목적인 hexview 프로그램이 C언어로 완성되었습니다.
그럼 이만 마치겠습니다.
전체 코드는 아래와 같이 압축파일로 올려놓겠습니다.
'코딩 > C와 C++' 카테고리의 다른 글
[C++ 기초 강좌] 1. C++ 프로그램의 구조 (0) | 2021.03.06 |
---|---|
stdbool.h 과 stdint.h 강의 (0) | 2020.12.30 |
1편-예제를 통한 C 언어 기초 강의 (0) | 2020.12.29 |
C++에서 spdlog 를 활용해서 log를 좀 더 쉽게 해보기 (2) | 2020.12.11 |
4편, c++ 프로그래밍 CMake 예제 (0) | 2020.12.05 |