코딩/C와 C++

2편-예제를 통한 C 언어 기초 강의

드리프트 2020. 12. 30. 10:02
728x170

 

 

안녕하세요?

 

지난 시간에 이어 예제를 통한 C언어 기초 강의 2편을 시작해 보겠습니다.

 

1편은 아래 링크 참고 바랍니다.

https://cpro95.tistory.com/118

 

1편-예제를 통한 C 언어 기초 강의

C는 프로그래머라면 가장 먼저 배우는 언어인데요. 저도 중학교 2학년때 부터 독학으로 Turbo C 책을 독파한 적이 있습니다. 정말 애증의 언어이며, 프로그래밍의 가장 기초적인 언어라고 생각합

cpro95.tistory.com

 

그럼 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언어로 완성되었습니다.

 

그럼 이만 마치겠습니다.

 

전체 코드는 아래와 같이 압축파일로 올려놓겠습니다.

 

hexview.zip
0.01MB

 

 

그리드형