이클립스 CDT 디버거를 통해 C++ STL 컨테이너의 내용을 보면 한눈에 봐서는 알 수 없는 난해한 구조와 값들로 가득차 있다. 하지만 pretty-printing을 통해 std::map, std::list, std::vector 등의 컨테이너 안에 있는 내용을 보다 가독성이 높은 형태로 볼 수 있다.


본 포스팅은 http://wiki.eclipse.org/CDT/User/FAQ#How_can_I_inspect_the_contents_of_STL_containers.3F 에서 제공하고 있는 내용을 한글로 간단하게 번역하고 적용하면서 겪었던 몇몇 참고 사항을 추가했다. 보다 정확한 정보를 원하시는 분은 위 링크를 직접 확인해 보시는 것도 좋은 방법이다.


Pretty-printing 요구사항 :

 - GDB 7.0 버젼 이상

 - python

 - pretty-print 소스 코드


Pretty-printing을 위해 GDB 설정하기


1. 파이썬이 설치 되어 있어야 한다.

svn을 통해 Python pretty-printers 를 체크아웃 받는다.

svn co svn://gcc.gnu.org/svn/gcc/trunk/libstdc++-v3/python

개인적으로는 프로젝트 하위 폴더에 받아 각각 관리하는 것도 좋지만 /usr/local 과 같은 시스템 디렉토리에 받아 놓고 프로젝트에 참여 하는 모든 사람들이 같은 패스를 가지게 하는것도 괜찮은 방법이라 생각한다.


2. GDB에게 pretty-print의 위치를 알려주기 위해 아래와 같이 .gdbinit을 작성한다.

python

import sys

sys.path.insert(0, '/home/user_name/gdb_printers/python') #svn에서 체크아웃 받은 디렉토리를 적어준다

from libstdcxx.v6.printers import register_libstdcxx_printers

register_libstdcxx_printers (None)

end

참고적으로 sys.path에 등록되는 디렉토리 경로는 절대 경로여야 하며 프로젝트 마다 매번 경로를 설정해주는 번거로움을 줄이기 위해 다음(http://stackoverflow.com/questions/8663076/python-best-way-to-add-to-sys-path-relative-to-the-current-running-script) 과 같은 방법을 써봤지만 정상적으로 인식을 하지 못한다. 공동 프로젝트를 각 개인이 개발하는 형식이라면 각각 .gdbinit을 셋팅해야 한다(방법을 알면 가같이 공유 좀..)


3. CDT에게 .gdbinit 파일이 어디있는지 알려줘야 한다. 

기본 경로는 해당 프로젝트의 루트 경로지만 이클립스 메뉴의 Windows->Preferences->C/C++->Debug->GDB에서 경로를 수정 할수도 있다.


참고 :

gdb가 무한루프에 빠지며 hang 되는 상태가 발생하는 버그가 있다고 한다(https://sourceware.org/bugzilla/show_bug.cgi?id=12555)

이럴 땐 <pretty_print_root_dir>/libstdcxx/v6/printers.py 파일 StdStringPrinter 클래스 to_string 함수 내에서 len 변수의 값을 적절한 크기로 제한 해주면 된다.

if len > 100:

len = 100

 

Posted by kukuta

댓글을 달아 주세요

들어 가기 전에.. :

- 이 문서는 Lex & Yacc의 기본적인 사용 방법에 대해서 다루고 있다.

- C/C++, 정규표현식, BNF에 대한 기본적 지식을 알고 있다는 가정하에 쓰여 졌다.


1. Lex & Yacc 의 목적

  • 일반적으로 Lex & Yacc는 컴파일러 또는 인터프리터를 만들기 위해 많이 사용되어지고 있다.
  • Lex & Yacc 는 독립된 어플리케이션이며, Lex는 특정 패턴의 문자열 토큰을 찾는 것, Yacc는 해당 토큰들의 관계를 분석하여 구문 검사를 하는 것으로 구분 되어있다.
  • 사용자는 Lex와 Yacc를 적절히 이용하여 주어진 입력(stdin, file 등등)에서 특정 토큰과, 구문을 찾아 낼 수 있으며, 이를 위해서는 정규 표현식과, BNF 문법에 대해서 기본적 지식을 가지고 있어야 한다.
  • 정규 표현식과, BNF를 이용하는 Lex & Yacc는 파싱에 대한 룰을 직접 작성하는 것보다 보다 나은 개발 속도 향상과 유지 보수 비용 절감에 도움을 준다.

2. Lex

  • 이 장에서 우리는 Lex 코드의 구성과, 작성 방법에 대해서 알아보도록 하겠다.
  • Lex는 특정 패턴의 토큰을 찾기 위해 정규 표현식을 이용한다. 정규표현 식에 대한 자세한 설명은 정규 표현식(Regular Expressions) 를 참고 하도록 한다.
  • Lex는 definition section, rules section, user-subrutine section 세개의 섹션으로 나뉘어 지며 각 섹션은 %%으로 구분된다.

    ... definition section ...

    %%

    ... rules section ...

    %%

    ... user-subrutine section ...


2.1 Definition Section

  • Definition Section은 literal block, start state code, definitions 파트 등으로 구성된다.
  • literal block
    • %{ 와 %} 로 구분된 블록은 literal block 라고 하며, 내부에 위치한 코드들은 Lex에 의해 검사 되지 않고 그대로 결과 C 파일의 초반부에 기록 된다.
    • include, 변수 선언, 함수 선언 등을 literal block 내에 적으면 된다.
  • start state code
    • Lex 룰에 start state를 지정한다는 것은, 특정 상태에서만 해당 룰이 적용되라는 것을 의미하고, start state code의 정의는 definition section에서 담당한다.
    • %s STATE1 STATE2 ... 로 표현 되며, Lex의 현재 상태를 나타내는데 사용 될 수 있다.
    • Lex의 상태(state)를 변경하기 위해서는 BEGIN 매크로를 사용하여야 하며, 사용 방법은 아래와 같다.

BEGIN STATE1

    • 원래의 기본 상태로 되돌아가기 위해서는 BEGIN 0 혹은 BEGIN INITIAL을 지정한다. 실 사용예는 Rules Section을 참고 하도록 한다.
  • definitions
    • definition 파트는 각 정규표현식에 이름을 지정하여, 동일한 정규 표현식을 재 작성 필요 없이, 사용 할 수 있도록 한다.
    • NAME expression 와 같은 형태로 표시 될 수 있으며, 공백 문자로 구분된다.
    • Definition Section의 끝은 %% 마크로 표시한다.

%{ // literal block begin


//  Lex.l file

#include <stdio.h>


char _gStr[4096];

int  _gInt;


%} // literal block end


%s S_COMMENT S_INT // state declaration


// definitions begin

varname [A-Za-z_][A-Za-z0-9_]*

integer [0-9]+

// definitions end

%%


2.2 Rules Section

  • Rules Section은 지정된 토큰을 발견 하였을 때 어떻게 동작해야 하는 지에 대해서 기술한다.
  • 각 룰은 토큰에 대한 정규표현식을 기술한 패턴 파트와 해당 토큰의 처리를 기술한 액션 파트로 구성된다.
  • 패턴 파트와 액션 파트는 공백 문자로 구분 된다.
  • Lex는 룰에 표현된 패턴의 토큰을 발견시 액션 파트에 있는 C코드를 결과 파일에 복사한다.
  • Rules Section의 끝은 %% 마크로 표시 한다.

<S_INT>{varname} { printf("This is a Varname which is behind of the Integer\n");

 BEGIN INITIAL;

}


{integer} { printf("This is an Integer\n"); 

 BEGIN S_INT;

}


{varname} { printf("This is a Varname\n"); }


%%


2.3 User-subroutine Section

  • User-subroutine Section은 결과 c 파일 끝 부분 Lex 관련 코드 뒤에 복사된다.

2.4 Lex 주요 함수

  • yylex() 
    • Lex에 의해서 만들어지는 스캐너는 yylex() 함수를 엔트리 포인트로 가진다.
    • yylex() 함수 리턴 후에 다시 yylex() 함수를 호출하게 되면 이전 스캔 했던 다음 부분부터 스캔하기 시작한다.


3. Yacc

  • 이 장에서는 Yacc 코드의 구성과, 작성 방법에 대해서 알아보도록 하겠다.
  • Yacc의 기본 구조는 Lex와 동일하며, 다만 각 Definition Section 에서의 선택적인 선언 사항들과, Rule Section 에서의 룰을 기술하는 방법이 다르다.
  • Yacc는 Lex와 마찬가지로 크게 세개의 섹션으로 구성되어 있으며, 각 섹션의 구분은 %% 마크를 이용한다.

    ... definition section ...

    %%

    ... rules section ...

    %%

    ... user-subrutine section ...

  • Yacc는 Lex의 엔트리 포인트인 yylex() 함수를 내부적으로 호출하여 스캔된 결과물을 넘겨 받으며, 이 결과물을 토큰이라고 말한다.
  • 실제로 yylex() 함수가 리턴하는 것은 단순 정수 값이다. Lex와 Yacc 간의 데이터 교환에 관한 내용은 다음 장에 다루도록 하겠다.
  • 각 토큰은 각자의 타입이 있을 수 있으며, 각 토큰 타입에 적절한 데이터 타입을 가질 수 있다.
  • Yacc에서는 각 토큰에 맞는 데이터 타입을 자동 혹은 편하게 지정할 수 있도록 하기 위해 몇가지 도움이 되는 옵션과 방법을 제공하고 있고, 다음에서 자세히 설명하도록한다.

3.1 Definition Section

  • Yacc도 Lex와 같은 literal block을 가지고 있으며, 사용법 또한 Lex와 동일하다. literal block 내부에 있는 코드들은 Yacc에 의해 검사 되지 않으며, 그대로 결과 C 파일에 복사된다.
  • %union
    • Yacc에는 다양한 토큰들이 존재 할 수 있고, 각 토큰에 관계되는 데이터를 저장하기 각각에 맞는 데이터 타입을 사용해야 할 수도 있다.
    • Yacc는 union 구조체를 이용하여 토큰에 관계되는 데이터 타입을 등록하고 사용 할 수 있도록 지원하고 있으며, 기본 사용법은 아래와 같다.

%union {

... field declarations ...

}

    • field declarations 에는 Yacc에서 각 토큰들에 적용되는 모든 데이터 타입들을 기술 할 수 있다.
    • field declarations 에 기술된 변수들은 union 구조체로로 변환되고, 결과 C 파일에 선언된다. 
    • 위에서 선언된 union 구조체는 YYSTYPE라는 티입으로 typedef 되어 Lex와의 통신 및 기타 토큰 데이터 저장등에 사용 될 수 있다.
    • %union을 사용하지 않는 경우에는 Yacc는 디폴트로 int 값을 정의 한다.
  • %union 사용 예

%union {

const char* str;

Token* tok;

}

    • 여기에서 지정된 각 타입의 이름들을 이용해 뒤에 설명되는 %token, %type에서 각 토큰을 값을 담을 변수의 타입을 지정 할 수 있다.
  • %token
    • Yacc는 Lex와 통신하기 위해 여러 개의 토큰 아이디를 정의 할 수 있으며, 문법은 아래와 같다.

%token <str> ADDCODE VARNAME COMMENT INTEGER INCLUDE

%token CLASS CHOICE OPENB CLOSEB SEMI COLON LESS MORE COMMA OPENBRACKET CLOSEBRACKET TYPEDEF

    • 이렇게 선언된 토큰 아이디들은 자동으로 정수를 부여 받아 define 형태로 C 파일에 기록된다.
    • %token<str> 과 같이 토큰에 대해 데이터 타입을 지정 할 때는, %union 섹션에서 등록된 이름만 사용 할 수 있으며, 그렇지 않은 경우 결과 *.c 파일을 컴파일 할 때 오류가 발생하게 된다.
    • 이 토큰 아이디들은 yylex() 함수에서 리턴 값으로 사용 될 수 있으며, Yacc는 아이디에 따라 액션을 달리 지정 할 수 있다.
    • 자세한 사용예는 Yacc Rules Section 을 다루는 장에서 다시 살펴 보도록 하겠다.
  • %type
    • Yacc는 토큰외에도 토큰과 토큰의 관계를 정의하고 있는 많은 규칙들이 있을 수 있고, 이 규칙 혹은 규칙의 집합을 statement라고 한다.
    • %type은 statement의 데이터를 저장 할 수 있는 데이터 타입을 지정하는데 사용되며 문법은 %token과 비슷하다.

%type <tok> classStmt choiceStmt setStmt includeStmt typedefStmt

%type <tok> vartype varDeclare VarDeclares

    • %type 역시 %union에 기술된 데이터 타입만을 사용 할 수 있으며, 사용법 역시 %token 처럼 이름을 <, > 사이에 기술한다.
    • Yacc의 Definition Section 역시 Lex의 그것 처럼 %% 마크를 이용하여 종료 표시를 한다.
  • Definition Section 예

%{ // literal block

#include "stdafx.h"

#include "lexer.h"


int bFile = 1;

char* y_str = NULL;

int _mylineno = 1;

void yyerrorEx( int nLineNo, const char *text, const char* szErrCode );


%}


%include {

#include "absyn.h"

#include "ParseTree.h"

}


%union { // union block. 여기에 기술된 타입과 변수는 union 구조체 형태로 .c 파일에 생성된다.

const char* str;

Token* tok;

}


// place any declarations here

%type <tok> Stmts

%type <tok> stmt Stmts

%type <tok> classStmt choiceStmt setStmt includeStmt typedefStmt

%type <tok> vartype varDeclare VarDeclares

%token <str> ADDCODE VARNAME COMMENT INTEGER INCLUDE

%token SET CLASS CHOICE OPENB CLOSEB SEMI COLON LESS MORE COMMA OPENBRACKET CLOSEBRACKET TYPEDEF

%%


3.2 Rules Section

  • Yacc의 Rules Section은 Lex의 그것과 비슷한 역할을 한다. 단, 차이점은 Lex의 Rules Section은 특정 토큰을 스캔 했을 때 지정된 액션을 실행 했지만, Yacc에서는 Lex로 부터 넘어온 각 토큰간의 적절한 관계(문법)을 발견 할 경우에만 액션을 실행한다는 것이다.
  • Rules Section은 지정된 문법을 파싱 하였을 때 어떻게 동작해야 하는 지에 대해서 기술한다.
  • 각 룰은 룰을 대표하는 이름과 콜론, 토큰 간의 관계를 정의하고 있는 정의 부분, 해당 문법을 파싱 했을 때 실행되는 액션, 룰의 끝을 나타내는 세미콜론으로 이루어 진다.

STATEMENTNAME : TOKEN1 TOKEN2 TOKEN3 { printf("yacc : action1 : %s, %d, %s\n", $1, $2, $3); $$ = $1; }

              : TOKEN1 TOKEN3 TOKEN2 { printf("yacc : action2 : %s, %s, %d\n", $1, $2, $3); $$ = $1; }

     ;

    • STATEMENTNAME 는 해당 룰의 이름을 나타낸다. 여기에서 기술된 이름 이용하여 또 다른 상위 룰의 하위으로 사용 할 수 있다.
    • TOKEN1 TOKEN2 TOKEN3 은 Lex가 리턴하는 토큰 아이디들이다. 이 아이디들에 대한 정보는 Lex의 definition section에서 %s 에 정의된 값들을 참조하게 된다.
    • TOKEN1 TOKEN2 TOKEN3 .. 은 각 토큰 간의 관계를 기술 한 것이다. 
    • 이 예에서는 TOKEN1 뒤에 TOKEN2가, TOKEN2 뒤에 TOKEN3이 나오는 경우 action1 이라고 출력하고, TOKEN3 이 먼저 나오고 TOKEN2가 나오는 경우는 action2 를 출력하도록 정의 되어 있다.
    • 위의 예제에서 $$, $1, $2, ... 와 같이 앞에 $ 마크가 붙은 것들은 각 토큰의 값을 저장하고 있는 변수들을 나타내는 것이다.
    • $1은 가장 첫번째 토큰인 TOKEN1을 $2는 두 번째인 TOKEN2를 의미한다. $$는 STATEMENTNAME의 데이터 타입 즉, 결과로 생성되는 non-terminal symbol의 데이터를 저장하는 변수를 나타낸다.
    • Yacc는 각 $x 에 필요한 데이터 타입을 결정하기 위해, 이전에 살펴 보았던 %token, %type을 참조하게 된다. 예를 들어 %type<tok> TOKEN1 이라고 type이 정의 했다면, $1은 c 코드로 변환 될 때 Token* 타입으로 인식되며, token 역시 비슷한 과정을 가지게 된다.
  • Rules Section 예

// token과  type에 대한 정의는 이전의 definition section을 살펴 볼 것

varDeclare : vartype VARNAME ';' { ;

printf("yacc : varDecl : Ordinary type : vartype VARNAME ;\n"); }


| vartype VARNAME '[' INTEGER ']' ';' { ;

printf("yacc : varDecl : Array type : vartype VARNAME [ INTEGER ] ;\n"); }

| COMMENT

printf("yacc : varDecl : Comment type : COMMENT\n"); }

;


vartype : VARNAME { ;

TRACE("yacc:varname:%s\n", $1); 

$$ = MakeVariableToken($1); 

}

| VARNAME LESS VARNAME MORE { ;

TRACE("yacc:container<data_type>\n"); 

$$ = MakeListToken(($1, $3);

}

| VARNAME LESS VARNAME COMMA VARNAME MORE { ;

TRACE("yacc:container<data_type1,data_type2>\n"); 

$$ = MakeMapToken($1, $3, $5);

}

;


3.3 User-subrutine Section

  • Lex와 동일

3.4 Yacc 주요 함수

  • yyparse()
    • 파싱을 하기위해서는 이 함수를 호출한다. yyparse()는 내부적으로 yylex() 호출하여 위에서 정의한 문법 규칙에 따라 적절한 결과 C 파일을 생성해 낸다.

4. Lex 와 Yacc 간의 통신

  • Lex와 Yacc 간의 통신에는 yylval 이라는 미리 정의된 전역변수가 이용된다.
  • yylval은 YYSTYPE 로 typedef 된 union 구조체로써 Yacc의 Definition Section의 %union 블록에 기술된 내용을 토대로 결과 파일에 생성된다.
  • Lex에서는 yylval을 이용하기 위해 Yacc의 결과 헤더 파일을 include 하여 yylval에 정의된 변수를 이용하여 토큰에 필요한 데이터를 전달한다.
  • yylex()에서 특정 패턴의 토큰을 발견했다고 Yacc에게 알리기 위해서는 Lex의 definition section 의  %s 에 정의된 선언들을 이용한다. 
  • Yacc는 Lex에서 리턴하는 토큰 아이디를 이해하기 위해 Lex의 결과 헤더 파일을 include해야 한다.
  • Lex에서 Yacc로 넘어온 토큰의 데이터들은 $$, $1, $2 같이 $x의 형태로 Yacc에게 전달 된다.

5. Example of Lex & Yacc

  • 설명에 들어가기 앞서 이전 장에서 살펴 보았던 몇가지를 짚고 넘어 가도록 하자. 이전 장에서는 Lex는 스캐너, Yacc는 파서라고 간단히 정의하고 넘어갔다. 하지만 좀 더 정확하게 말하자면, Lex & Yacc 는 스캐너 코드와 파서 코드를 만들어 내는 일종의 컴파일러다. 

    lex 에 의해 생성되는 sanner를 lexer라고도 하며, yacc에서 만들어 지는 것을 parser라고 한다.

Lexer(scanner) -------> Parser --------------> code generation

       ^         token      ^    (syntax tree)

       |                    |      

    regular exp.         grammer

  • 개략적인 순환 구조는 위와 같으며, lexer가 정규표현식을 이용하여 특정 토큰을 찾아 내면 파서는 그 토큰들을 모아 하나의 syntax tree를 구성하는 역할을 한다.그리고 전체 syntax tree가 완성되면 사용자는 그 트리를 이용하여 다양한(자기가 원하는 어떠한 일이든) 일을 할 수 있다.

5.1 Lex Definition Section Code Sample

      1 %{

      2 /****************************************************************************

      3 Lexer.l

      4 ****************************************************************************/

      5 #include "y.tab.h"

      6

      7 char _addcode_string[4096];

      8 int _addcode_pos;

      9 %}

     10

     11 %s S_ADDCODE

     12

     13 varname     [A-Za-z_][A-Za-z0-9_]*

     14 integer     [0-9]+

     15 class       "class"

     16 choice      "choice"

     17 openb       "\{"

     18 closeb      "\}"

     19 semi        ";"

     20 addcode     "%%"

     21 newline     "\n"

     22 whitespace [ \t\n\r]

     23 %%

    • 1~9 : %{ 와 %}로 감싸져 있는 부분을 literal block 이다. 이 안의 내용은 아무런 여과 없이 c 파일로 복사된다.
    • 11 : %s 는 start state를 기술 한다. 여기서는 S_ADDCODE 하나만을 선언했으나, 공백을 두어 여러개의 start state를 선언 할 수도 있다.
    • 13 ~ 2 : 복잡한 정규 표현식을 특정 이름과 맵핑 시켜 놓은 일종의 '정규식 선언 테이블'이다. 여기에 선언된 이름들은 rules section에서 사용 될 수 있다.
    • 위의 lex 코드를 lex 컴파일러로 컴파일 하게 되면, 헤더 파일에는 S_ADDCODE define 형태로 기록되고, c 파일에는 literal block에 있는 내용이 복사 된다. 아래는 결과 파일의 일부를 복사한 것이다 :

file : lex.yy.c

{{{

#include "y.tab.h"

char _addcode_string[409600];

int _addcode_pos;

}}}


file : lex.yy.h

{{{

#define S_ADDCODE 1000

}}}

※ 참고로 여기서 lex.yy.h, lex.yy.c 와 같은 결과 파일이름은 lex의 버젼, 입력 파라메터에 따라 다를 수 있다.


5.2 Lex Rules Section Code Sample

     .. ... Definition section ...

     23 %%

     24

     25 <S_ADDCODE>{addcode} {

     26                         _addcode_string[_addcode_pos] = 0;

     27                         BEGIN INITIAL;

     28                         yylval.str = _addcode_string;

     29                         _addcode_pos = 0;

     30                         return ADDCODE;

     31                     }

     32 <S_ADDCODE>.|"\n"   { _addcode_string[_addcode_pos++] = (unsigned char)yytext[0]; }

     33 {addcode}           { _addcode_pos = 0; BEGIN S_ADDCODE; }

     34 {class}         { return CLASS; }

     35 {choice}        { return CHOICE; }

     36 {openb}         { return OPENB; }

     37 {closeb}        { return CLOSEB; }

     38 {semi}          { return SEMI; }

     39 {colon}         { return COLON; }

     40 {varname}       { yylval.str = strdup(yytext); return VARNAME; }

     41 {whitespace}+   { ; }

     42 .|\n            { printf("Something wrong!") }

     43 %%

    • 33 : {addcode}라고 적혀 있는 패턴 부분을 보도록 하자. { ... }는 definition section에서 그 정의를 찾아 치환 하겠다라는 의미이며, 이와 같은 경우에는 "%%" 패턴이 여기에 적용된다.
    • 33 라인의 액션 파트에서 lexer의 현재 start state가 S_ADDCODE로 변경 되며, 이로써 start state가 다른 것으로 변경 되기 전까지는 앞에 <S_ADDCODE>가 붙은 조건만이 적용된다.
    • 32 : < ... > 는 스캐너가 특정 start state에 있는 경우만 뒤의 패턴을 검사한다고 했다. 이와 같은 경우 33라인에서 start state가 변경 되었으므로 이제 부터는 32라인과 25 라인의 두 조건 중에 하나가 선택되게 된다.
    • '.' 문자는 개행 문자를 제외한 모든 문자에 매칭 되는 표현 식이며, \n는 개행 문자를 의미 한다. 단, 25라인에서 %% 가 있는지 먼저 검사하고 있으므로, 결론적으로 %%를 제외한 스캔되는 모든 문자들을 _addcode_string 이라는 배열에 저장한다.
    • 25 ~ 31 : 이 조건은 다시 한번더 '%%'를 스캔하는 경우에 만족하게 된다. 액션 파트에서는 33라인에서 변경된 start state(S_ADDCODE)를 초기 상태로 돌린다(BEGIN INITIAL). 
      그리고 지금 까지 저장했던 _addcode_string의 값을 yylval.str 이라는 전역 변수를  이용하여 yacc에게 전달하는 동시에 ADDCODE 를 리턴함으로써 yacc에게 어떤 토큰이 스캔 되었는지 알려 준다.
    • 33 ~ 39 : 특정한 조건 없이 위에서 패턴 파트에 기술된 문자열을 만나면 지정된 코드를 리턴하거나 해당 토큰을 무시한다.
    • 위의 예제에서 ADDCODE, CLASS  같은 리턴 코드들과, yylval 같은 변수들은 lex 코드의 그 어디를 찾아 보아도 해당 내용에 대한 설명이나 흔적은 찾을 수 없다. 이 변수와 define 매크로는 yacc 파일에 정의된 것으로써 lex의 definition section에서 include 했던 "y.tab.h" 파일에 선언되어 있다. (yacc에 의해 생성되는 헤더 파일의 이름은 각 버젼, 사용자의 입력 파라메터에 따라 다를 수 있다.)

5.3 yacc Definition Section Code Sample

  • 이전 장에서 lex 코드에서 yacc 쪽을 참조한다고 말했다. 지금 부터는 yacc의 Definition section을 살펴보도록 하겠다.

      1 %{

      2 /****************************************************************************

      3 Parser.y

      4 ****************************************************************************/

      5 #include "lexer.h"

      6 %}

      7

      8 %include {

      9 #include "ParseTree.h"

     10 }

     11

     12 %union {

     13     const char* str;

     14     Token* tok;

     15 }

     16

     17 %type <tok> Stmts

     18 %type <tok> stmt Stmts

     19 %type <tok> classStmt choiceStmt

     20 %type <tok> vartype varDeclare VarDeclares

     21 %token <str> ADDCODE VARNAME

     22 %token CLASS CHOICE OPENB CLOSEB SEMI

     23 %%

  • 위의 코드에서 literal block (1~6 라인) 내에 있는 코드들은 yacc가 생성하는 c 파일의 처음 부분에 기록되며, 나머지는 모두 header파일에 기록 된다.
  • 8~9 : 기본적인 역할은 literal block과 동일하다. 하지만 차이점은 %include 블록 안의 내용은 헤더파일의 처음에 기록되고, literal block의 내용은 c 파일의 처음에 기록된다는 것이다.
  • yacc의 밴더나 버젼별로 지원하는 경우도 있고, 그렇지 않은 경우가 있다. 사용하기 전에 지원 여부를 미리 확인하고 사용하여야 한다. 만일 지원하지 않는 경우라면 ParseTree.h 에 있는 타입을 쓰는 모든 파일에서 include를 각자 해야 하며, 전역 변수의 경우는 extern 과 같은 방법으로 우회 할 수도 있다.
  • 12 ~ 15 : %union 블록 내의 코드는 아래와 같은 코드로 재 탄생 된다.

union tagYYSTYPE {

const char* str;

Token* tok;

};

#define YYSTYPE union tagYYSTYPE

extern YYSTYPE YYNEAR yylval;


  • 자동적으로 전역변수 yylval이 선언되고, lex에서 yacc로 토큰의 데이터를 yacc로 전달하기 위해 사용되어질 수 있다.
  • 여기서 사용된 Token* 과 같은 자료 구조는 %include에서 추가 되는 ParseTree.h 와 같은 헤더 파일에 정의 될 수 있다.
  • 17 ~ 22 : %type 과  %token들은 define 형태로 변환되어 헤더파일에 기록된다. 역시 lex에서 특정 토큰, 즉 terminal-symbol을 스캔 했다는 의미로 리턴되는 코드로 사용될 수 있다.

5.4 yacc rules section.


     .... Definition section ....

     23 %%

     24 Stmts    : stmt              { g_pRoot->append($1); // 'g_pRoot' is a global variable declared somewhere in ParseTree.h }

     25             | Stmts stmt        { g_pRoot->append($2) }

     26             ;

     27 stmt      : classStmt         { $$ = new Token($1); }

     28             | choiceStmt        { $$ = new Token($1); }

     29             | ADDCODE           { $$ = new Token($1); }

     30             ;

     31 classStmt   : CLASS VARNAME OPENB VarDeclares CLOSEB SEMI   { $$ = new Token("class", $2, "{", $4, "}", ";"); }

     32                  | CLASS VARNAME OPENB CLOSEB SEMI               { $$ = new Token("class", $2, "{", NULL, "}", ";");  }

     33                  ;

     34 choiceStmt  : CHOICE VARNAME OPENB VarDeclares CLOSEB SEMI  { $$ = new Token("choice", $2, "{", $4, "}", ";"); }

     35                   ;

     36 VarDeclares : varDeclare{ $$ = new Token($1); }

     37                   | VarDeclares varDeclare { $$->append($2); }

     38                   ;

     39 varDeclare  : vartype VARNAME SEMI  { $$ = new Token($1, $2, ";"); }

     40                  | ADDCODE           { $$ = new Token($1); }

     41                  | vartype VARNAME varDeclare { ; yyerrorEx(_mylineno-1,"missing ';'","C2143" ); }

     42                  ;

     43 vartype  : VARNAME   { $$ = new Token($1); }

     44             ;

     45

     46 %%

  • 위는 축약된 형태의 rules section이다. 원래는 액션 파트에 syntax tree를 만드는 c 좀더 정확하고 복잡한 코드가 추가 되어야 하지만, 설명을 위해 간단하게 축약된 형태로 기록했다.

    그리고 아래는 설명을 위해 임의적으로 만든 유사 C++ 코드다. 아래 코드를 어떻게 스캔하고 파싱하는지 위에서 작성한 룰들이 어떻게 동작하는지 알아보도록 하자.

      1 %%

      2 /**

      3     This is Just a Comment!!

      4 */

      5 %%

      6

      7 class FooQuestion {

      8     int     trackingNumber;

      9     string  question;

     10 };

     11

     12 class FooAnswer {

     13     int questionNumber;

     14     BOOL    answer;

     15 };

     16

     17 choice FooProtocol {

     18     FooQuestion msgFooQuestion;

     19     FooAnswer   msgFooAnswer;

     20 };

  • 1 ~ 5 : lexer가 이 부분을 스캔하게 되면 ADDCODE 를 리턴한다. yacc는 ADDCODE를  리턴 받는 경우, 이것이 28 라인에서의 ADDCODE인지 39 라인 varDeclare 에서의 ADDCODE인지 다음에 리턴되는 코드를 보고 판단하게 된다. 이와 같은 경우 다음 symbol로 class 가 리턴 되므로 Definition section 27 라인 stmt 룰을 적용하게 된다.
  • 7 ~ 10 : stmt 룰 적용 후, CLASS가 리턴되고 그 후 VARNAME OPENB VarDeclares CLOSEB SEMI  순서대로 리턴된다.
  • VarDeclares 는 36 ~ 37 라인 조건에서 재귀적으로 자신을 호출함으로써 무한 길이의 varDeclare 를 받아 들일 수 있다.
  • varDeclare 는 vartype VARNAME SEMI 이며 이는 클래스 내에 선언되어 있는 멤버 변수 선언과 동일한 순서다.만일 symbol 들이 리턴 되는 순서가 틀린다 던지, 예를 들어 class 뒤에 바로 choice가 나온 경우, 존재 하지 않는 토큰이 발견되는 경우, lex나 yacc에서 적절한 에러를 발생 시킨다.
  • 이렇게 FooQuestion, FooAnswer를 위한 파서 트리를 만들고, choice 에 대한 것 역시 비슷한 수순으로 파싱이 끝나면 적절한 syntax tree를 생성 할 수 있게 된다.
  • 일반적으로 syntax tree를 구성하고 접근하기 위해 사용되는 포인터 같은 경우, 전역 변수로 선언되어 전체 프로그램에서 사용되는 경우가 많다. yyparse() 를 호출하는 경우를 보자면, 아래와 같이 yacc의 user sub-routine section에 기술 될 수 있다 :

     ... yacc rules section ...

     48 %%

     49 Token* MakeParseTree(const string& file)

     50 {

     51     bFile = 1;

     52

     53     yyin = fopen(szFilePath.c_str(), "r");

     54     if(yyin == NULL)

     55     {

     56         yyerror("can't open file");

     57         return NULL;

     58     }

     59     yyparse();

     60     fclose(yyin);

     61     return g_pRoot; // g_pRoot is a global variable declared somewhere in ParseTree.h

     62 }

  • 53 : yacc 에서 제공 하고 있는 입력 파일 포인터 yyin에 기본 stdin 대신 특정 파일의 포인터로 교체 한다.
  • 59 : yyparse() 함수를 호출 하고 있다. yyparse() 함수는 파일 스트림을 다 읽을 때 까지 내부적으로 루프를 돌며, 파싱 과정에서 각 룰에 맞는 syntax가 발견 될 때 마다 해당 룰의 action part를 실행한다.
  • 각 action part 에서는 Token이라는 구조체에 각자의 데이를 저장하며 최종에는 전역 변수인 g_pRoot에 syntax tree의 root를 저장하는 형태다.  여기서 g_pRoot는 임의적으로 지정한 전역변수로써 yacc에서 우리가 작성하는 어플리케이션으로 데이터를 전송하기 위해 사용되어 진다.
  • yacc를 사용하는 어플리케이션은 g_pRoot를 이용하여 적적한 syntax tree에 대한 오퍼레이션을 할 수 있다.

이후 생성된 syntax tree를 어떻게 사용하는 지는 각 프로그램의 용도와 환경에 따라 여러 다양한 방법이 나올 수 있기에 본 문서에서는 여기까지 설명을 마치 마무리 하도록 하겠다.


6. 마치며

이상 lex & yacc 의 기본 개념 부터 기초 사용 방법까지 알아보았다.  어떻게 lex&yacc를 설치하고, 빌드하는지에 대해서는 밴더마다, 버젼마다 차이가 있으므로 자신이 쓰는 lex&yacc의 문서를 참고하도록 한다.

Posted by kukuta

댓글을 달아 주세요

다른 언어에서는 기본적인 verify 라이브러리들이 지원이 되는데 유독 C/C++에서는 관련 라이브러리가 없더군요. 
다행히 openssl이라는 라이브러리가 있어 그걸 사용하여 간단한 verify 함수를 만들어 보았습니다.
(코드는 몇줄 안되긴 하지만 샘플 코드라던지 자세한 설명이 없어 아래 코드를 만드는데 몇일 걸렸네요..;;)

본 포스트는 :
  • 구글 인-앱 결제를 어떻게 하는지 설명하지 않습니다. 결제 이후 나오는 signature를 검증하는 방법에 대해서 다룹니다.
    결제 방식을 알고 싶으시면 여기로->http://developer.android.com/google/play/billing/api.html#purchase)
  • Google Developer Console에 등록하는 방법, public_key를 얻는 방법, 상품을 등록하는 방법들에 대해 설명하지 않습니다.
  • C++ 언어 사용자를 대상으로 합니다

아래 코드를 읽기 위해선.. :
  • 샘플 코드의 signature, public_key, purchase_data는 당연히 가짜 입니다. 저대로 실행하면 0 리턴 합니다. 
  • 아래대로 빌드하면 Base64Decode 함수를 찾을 수 없다는 오류가 뜹니다. 적절한 코드 베끼시면 됩니다
  • 당연히 openssl 라이브러리 import 해야 합니다.
  • std::shared_ptr를 사용했으니 C++0x 혹은 C11 버젼 사용하셔야 합니다.
    (아니면 코드를 고치시면 됩니다. 각 변수들의 XXX_free 함수 보이시죠??)
  • 어차피 아래 샘플로는 빌드 안되는거 iostream 같은 자잘한 include는 당연히 알고 있다 생각하고 넘어갑니다.
  • OpenSSL_add_all_digests(), EVP_cleanup() 함수가 밖으로 빠져 있는 것에 주목해 주세요. 
    InappBillingVerify함수 안에 같이 들어가도 되지만 멀티 쓰레드에서 돌아가는 경우 에러를 발생 시킵니다. 저 두 함수는 thread-safe하지 않습니다.

Sample Code

#include <openssl/evp.h>
#include <openssl/pem.h>

int InappBillingVerify(const char* data, const char* signature, const char* pub_key_id)
{
    std::shared_ptr<EVP_MD_CTX> mdctx =
    std::shared_ptr<EVP_MD_CTX>(EVP_MD_CTX_create(), EVP_MD_CTX_destroy);
    const EVP_MD* md = EVP_get_digestbyname("SHA1");

    EVP_VerifyInit_ex(mdctx.get(), md, NULL);

    EVP_VerifyUpdate(mdctx.get(), (void*)data, strlen(data));

    std::shared_ptr<BIO> b64 = std::shared_ptr<BIO>(BIO_new(BIO_f_base64()), BIO_free);
    BIO_set_flags(b64.get(),BIO_FLAGS_BASE64_NO_NL);

    std::shared_ptr<BIO> bPubKey = std::shared_ptr<BIO>(BIO_new(BIO_s_mem()), BIO_free);
    BIO_puts(bPubKey.get(),pub_key_id);
    BIO_push(b64.get(), bPubKey.get());

    std::shared_ptr<EVP_PKEY> pubkey = 

std::shared_ptr<EVP_PKEY>(d2i_PUBKEY_bio(b64.get(), NULL), EVP_PKEY_free); std::string decoded_signature = Base64Decode(std::string(signature)); return EVP_VerifyFinal(mdctx.get(), (unsigned char*)decoded_signature.c_str(), 

decoded_signature.length(), pubkey.get()); } int main() { const char* purchase_data = "{" "\"orderId\":\"12999763169054705758.1371079406387615\"," "\"packageName\":\"com.example.app\"," "\"productId\":\"exampleSku\"," "\"purchaseTime\":1358227642000," "\"purchaseState\":0," "\"developerPayload\":\"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ\"," "\"purchaseToken\":\"rojeslcdyyiapnqcynkjyyjh\"" "}"; const char* public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAO..."; const char* signature = "DW0nEjsBhqHpECrS5Tcq6Y51vM..."; OpenSSL_add_all_digests(); std::cout << InappBillingVerify(purchase_data, signature, public_key) << std::endl; EVP_cleanup(); return 0; }

  • purchase_data

    • string in JSON format that is mapped to the INAPP_PURCHASE_DATA
    • 구글 인-앱 결제 완료 후 클라이언트가 서버로 보내온 결제 정보
    • packageName : 패키지 이름. 정상적인 app으로 부터 도착한 메시지인지 검증
    • product_id : 상품 아이디. 게임 서버에 상품아이디와 아이템을 매핑 시켜 놓고, 상품 아이디를 키로 삼아 아이템 지급
    • developerPayload : 개발시 임의로 넣는 값. 클라이언트 사이드에서 Google Play 구매 요청시 전송하면 결과 json에 포함되어 리턴. 매번 다른 값을 발급하여 인증 요청 중복 체크등에 사용
  • signature : Google Play 결제 결과로 리턴되는 암호화 된 값(RSA+SHA1, Base64 encoded, PKCS#1 padding)
  • public_key  
  • return
    • valid : 1
    • invalid : 0
    • error : -1

참고 : php 버젼. 결과가 이상하다 싶을 땐 이걸로 테스트 해보길 권장함.
https://github.com/mgoldsborough/google-play-in-app-billing-verification


Posted by kukuta

댓글을 달아 주세요

  1. 2013.11.18 15:57  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

    • Favicon of https://kukuta.tistory.com BlogIcon kukuta 2013.11.21 10:35 신고  댓글주소  수정/삭제

      뻑 난다는것이..
      메모리 참조 오류 같은 걸로 코어가 떨어지고 죽어 버린다는 것인가요? 아니면 정상적으로 처리되어야 하는데 이상하게 처리 된다는 것인가요?

      아마도 프로세스가 죽었다는 의미 같긴한데..
      openssl 구조체나 문자열에 null 값이 있는것은 아닌가 합니다.

      1. 넘어온 문자열에 null이 있다.
      2. 샘플 코드에서 빠진 초기화 코드 같은 것이 있어서 openssl 구조체에 null이 들어 있다.

  2. tolik 2013.11.20 21:54  댓글주소  수정/삭제  댓글쓰기

    Where should i get orderId?

    • Favicon of https://kukuta.tistory.com BlogIcon kukuta 2013.11.21 10:28 신고  댓글주소  수정/삭제

      after finishing the tranaction between 'google play' and your app.
      google play may return json structure.
      and it may contain the 'orderid'

    • tolik 2013.11.22 06:44  댓글주소  수정/삭제

      So its finish of transaction. Is there code in c++ of how should i initiate it? Thank you.

    • Favicon of https://kukuta.tistory.com BlogIcon kukuta 2013.11.25 11:44 신고  댓글주소  수정/삭제

      No c++ code to get 'orderId'. I think you should use JAVA code provided by Google.

      You may find 'orderId' in 'INAPP_PURCHSE_DATA'(is element of INAPP_PURCHSE_DATA_LIST).
      http://developer.android.com/google/play/billing/api.html#purchase
      see above link. In 'Purchasing Items' section, attention 'getPurchases()' call. Through it, you can take 'INAPP_PURCHSE_DATA_LIST' and 'INAPP_PURCHSE_DATA'.

      see below link Table 4 : http://developer.android.com/google/play/billing/billing_reference.html
      'INAPP_PURCHSE_DATA' has orderId.

      sample code is blow :
      http://stackoverflow.com/questions/14262230/getting-order-id-when-querying-for-purchased-items

    • Favicon of https://kukuta.tistory.com BlogIcon kukuta 2013.11.25 11:47 신고  댓글주소  수정/삭제

      if you have problem using JAVA, and should use C++.
      NDK may be the solution.

  3. youngi 2014.09.03 11:14  댓글주소  수정/삭제  댓글쓰기

    감사합니다.

갯수가 정해지지 않은 N개의 인자를 사용 할 수 있는 기능으로써 D언어와 C++ 11에서 지원하고 있다

(from. http://en.wikipedia.org/wiki/Variadic_template)


C++ 11 이전 버젼의 템플릿에서는 지정된 갯수만큼의 인자만을 받을 수 있었으나 C++11에서 부터는 임의의 갯수를 받을 수 있도록 변경 되었다. 기본적인 문법은 아래와 같다 :


template<typename... Values> class tuple;


위의 템플릿 클래스 tuple은 어떠한 타입이든 몇개든 상관 없이 템플릿 인자 생성이 가능 하다. 예를 들어서 위 클래스의 인스턴스로 아래와 같은 것이 가능하다 :


tuple<int, std::vector<int>, std::map<std::string, std::vector<int>>> some_instance_name;


물론 0개의 템플릿 인자로도 인스턴스 생성이 가능하고 tuple<>some_instance_name 과 같은 코드도 정상 동작한다. 만일 최소한 하나의 인자는 받아야 한다라고 강제하고 싶다면 아래와 같은 코드도 가능 하다 :


template<typename First, typename... Rest> class tuple;


위 코드에서는 tuple<> some_instance_name 과 같은 코드는 컴파일 에러를 발생 시킨다.


Variadic template은 템플릿 특화(Template specialize)와 결합해서 사용하면 가변인자를 이용한 함수(va_list) 와 같은 기능을 하지만 %c, %d와 같은 귀찮은 예약 인자와 순서를 맞춰야만 하는 귀찮음을 제거 할 수 있는 강력한 도구가 된다. 아래 코드는 임의의  타입으로 구성된 임의의 갯수의 인자를 받아 결합하여 문자열로 리턴 하는 함수를 variadic template을 이용하여 구현 한것이다 :

#include <string>

#include <sstream>

#include <iostream>

template <class T>

void Concat(std::stringstream& stream, const T& t) // 종료 템플릿 함수

{

        stream << t;

}


template <class T, class... ARGS>

void Concat(std::stringstream& stream, const T& t, ARGS&&... args) // 재귀 템플릿 함수

{

        stream << t;

        Concat(stream, args...);

}


template <class... ARGS>

std::string Concat(ARGS... args) // 유저 호출 함수

{

        std::stringstream stream;

        Concat(stream, args...); // 템플릿 특화로 인해 재귀 템플릿 함수가 호출 된다.

        return stream.str();

}


int main()

{

        int n100 = 100;

        int n200 = 200;

        float f100 = 100.0f;

        float f200 = 200.0f;

        std::string str = "string out";

        std::cout << Concat("string ", n100, " ", str, " ", n200, " ", f100, " ", f200 ) << std::endl;

           printf("string %d %s %d %f %f", n100, str.c_str(), n200, f100, f200);

        return 0;

}


ref :

 - http://en.wikipedia.org/wiki/Variadic_template

Posted by kukuta

댓글을 달아 주세요

서론 :

지금 까지 나는 byte padding이 cpu 레지스터 사이즈를 따라 일괄적으로 적용 되는 줄 알고 있었다(http://nsjokt.springnote.com/pages/4716509). 하지만 오늘 exe와 dll 사이에서 일어난 문제를 해결하며 찾아본 자료에서 byte padding은 아래의 네 가지 규칙에 따라 구조체에 따라 다르게 적용 된다는 사실을 알았다. 예를 들어 설명 하자면 지금까지 페이지 사이즈가 4byte인 어플리케이션에서는 byte padding 사이즈를 따로 지정해 주지 않는 한 1 byte 짜리 멤버 변수를 가진 구조체나 4 byte 멤버 변수를 가진 구조체의 sizeof 결과가 모두 4 byte로 같을 것이라고 생각 했으나 실제로는 각각 1 byte, 4 byte로 다르게 align 되고 있었다.

 

위에서 언급한 네 가지 규칙을 살펴 보자면(일단 VC 기준, gcc는 다른 옵션이 있겠지) :

  1. Unless overridden with __declspec(align(#)), the alignment of a scalar structure member is the minimum of its size and the current packing.
  2. Unless overridden with __declspec(align(#)), the alignment of a structure is the maximum of the individual alignments of its member(s).
  3. A structure member is placed at an offset from the beginning of its parent structure which is the smallest multiple of its alignment greater than or equal to the offset of the end of the previous member.
  4. The size of a structure is the smallest multiple of its alignment larger greater than or equal the offset of the end of its last member.

출처 : http://msdn.microsoft.com/ko-kr/library/83ythb65(v=vs.100).aspx

 

나름 쉽게 풀어쓴다고 썼지만 나도 알아 먹기 힘드니 예제를 통해서 알아 보도록 하자

 

1. Unless overridden with __declspec(align(#)), the alignment of a scalar structure member is the minimum of its size and the current packing.

 

"__declspec(align(#))을 오버라이드 하지 않으면, 스칼라 구조체(long, bool과 같은 일반 변수로만 이루어진 구조체)의 멤버는 변수의 사이즈와 현재 지정된 byte padding align을 따른다."

 

이해를 돕기 위해 아래와 같이 두 가지 타입의 구조체를 만들어 보았다 :

struct Size_1_Align
{
    bool b;
};

 

struct Size_4_Align
{
    long l;
};


 

int main()

{

std::cout << "Size_1_Align:" << sizeof(Size_1_Align) << std::endl;
std::cout << "Size_4_Align:" << sizeof(Size_4_Align) << std::endl;

 

output :

Size_1_Align:1
Size_4_Align:4

bool 는 1 byte, long은 4 btye, 각 구조체의 사이즈는 각각의 멤버 변수 사이즈 대로 만들어진다.

 

2. Unless overridden with __declspec(align(#)), the alignment of a structure is the maximum of the individual alignments of its member(s).

 

"__declspec(align(#))을 오버라이드 하지 않으면, 구조체는 멤버 변수중 가장 사이즈가 큰 멤버 변수의 byte padding align을 따른다."

 

그럼 위의 Size_1_Align 구조체에는 2 byte 변수를, Size_4_Align 구조체에는 long(4byte) 보다 작은 사이즈의 멤버 변수를 추가해 보겠다. 가장 큰 사이즈 멤버 변수의 align을 따른다고 했으므로 Size_1_Align은 2 byte align 규칙을 따를 것이고, Size_4_Align 구조체는 4 byte align 규칙을 따를 것이다 :

struct Size_1_Align
{
    bool b;  // byte padding 규칙에 따라 요 부분은 1 byte가 더 채워 질 것입니다.
    short s;
};

struct Size_4_Align
{
    long l;
    bool b;
   short s;

};

int main()
{
    std::cout << "Size_1_Align:" << sizeof(Size_1_Align) << std::endl;
    std::cout << "Size_4_Align:" << sizeof(Size_4_Align) << std::endl;
}

output :
Size_1_Align:4
Size_4_Align:8

예상대로 각각 4 bypte 와 8 byte 가 나왔다.

 

3. A structure member is placed at an offset from the beginning of its parent structure which is the smallest multiple of its alignment greater than or equal to the offset of the end of the previous member.

 

"구조체 멤버는 부모 구조체의 byte padding align의 시작 offset에서 부터 이전 멤버 변수의 끝 offset 뒤에 자리 잡는다."

 

여기서 부모 구조체의 byte padding align에 대해서 잠깐 설명하자면, 부모가 4 byte padding 규칙을 가지고 실제 7 바이트 멤버만 가지고 있는 경우 7 byte 보다 큰 4 의 배수 byte offset을 시작 점으로 잡는다는 의미다. 예를 들어 :

struct Size_4_Align
{
    long l;
    short s; // bool b 와 위치가 바뀌었음.

   bool b; // short s 와 위치가 바뀌었음.
};

 

struct Derived_4_Align : public Size_4_Align
{
    char c;
};


int main()
{
    std::cout << "Size_4_Align:" << sizeof(Size_4_Align) << std::endl;
    std::cout << "Derived_4_Align:" << sizeof(Derived_4_Align) << std::endl;
}

 

output :
Size_4_Align:8
Derived_4_Align:12

Derived_4_Align 구조체는 Size_4_Align을 상속 받아 1 byte(char) 짜리 변수를 추가 했다. 생각해 보면 Size_4_Align의 경우 멤버 변수 나열이 4, 2, 1 byte로 나열 되었으므로 뒤에 1 바이트가 추가 되면 8 byte로 align이 될 수도 있지 않을까 생각했지만 에누리 없이 기존 8 byte에 더하여 (그리고 최대 사이즈 멤버 변수가 4 byte이므로) 12 byte 짜리 구조체가 되었다.

 

4. The size of a structure is the smallest multiple of its alignment larger greater than or equal the offset of the end of its last member.

 

"구조체의 사이즈는 가장 마지막 멤버 변수의 offset 을 기준으로 align 되는 byte의 최소 배수이며 offset과 같거나 큰 사이즈를 가진다."

 

말이 조금 달라지긴 했지만 위 3번에서 설명 했던 byte padding align과 같은 의미다. 결국 구조체의 사이즈는 byte padding을 통해 가장 페이징 하기 좋은 사이즈로 결정 된다는 의미다.

struct Size_8_Align
{
    long long ll;
    bool b;
};

 

int main()
{
    std::cout << "Size_8_Align:" << sizeof(Size_8_Align) << std::endl;
}

 

output :
Size_8_Align:16

결국 가장 큰 멤버 변수 사이즈(8 byte)로 align 되는 구조체는 1 byte가 더 추가 되더라도 8 byte 가 증가 하며 (align 사이즈의 배수) 실제 구조체 사이즈인 9 byte 보다는 크다는 것이다.

 

결론 :

오늘의 핵심은 구조체의 byte padding은 모든 구조체에 일괄적용 되는 것이 아니라 규칙에 따라 각각의 최적의 align 사이즈를 가지고 있다는 것이다.

 

위 코드들은 vc++ 2010과 gcc 4.4.6 버젼 위에서 테스트 해보았다(혹시나 예전 버젼 컴파일렁에서는 padding byte가 일괄 적용 된것이 아니었나 싶어 버젼을 명시한다).

Posted by kukuta

댓글을 달아 주세요

  1. Favicon of http://blog.ggamsso.wo.tc BlogIcon 깜쏘 2012.11.13 10:23  댓글주소  수정/삭제  댓글쓰기

    이게 플랫폼이 제한적이라면 상관이 없는데
    HP-UX, AIX, Solaris에서 다 다르게 SigBus 뜨면 미치죠.

  2. Favicon of http://kukuta.tistory.com BlogIcon kukuta 2012.11.14 23:34  댓글주소  수정/삭제  댓글쓰기

    뭔소리야? 패딩에 왠 sigbus? 구조체를 char 포인터로 강제 캐스팅해서 쓰고 있냐?
    아니면 HP, AIX, Solaris에서 바이트 패딩 룰이 다 다르다는 이야기야? 그래서 네트웍으로 전송할 때 값이 깨진단 의미??

  3. Favicon of https://jangjy.tistory.com BlogIcon 매직블럭 2014.07.23 17:07 신고  댓글주소  수정/삭제  댓글쓰기

    패딩 관련 문제로 매우 궁금하던차에 적절한 글 잘 봤습니다.

    감사합니다

" error: too few template-parameter-lists"는 신규 버젼 gnu cpp 컴파일러에서 발생하는 에러다.
이 문제는 템플릿 클래스의 static 멤버 변수를 초기화 할 때 발생하며, 해결을 template<>을 static 멤버 변수 초기화 코드 앞에 붙여 주어야 한다.

예를 들어 :


template <class T>

 class A
 {
    static int a;
    static const char * const name;
 };

와 같은 코드가 있다고 해보자. 예전에는 아래와 같이 써도 무방했다 :
 int A::a = 0;
const char * const A::name = NULL;
하지만 위와 같은 코드는 
CeePlusPlus 표준에 의해 이제는 더 이상 유효한 코드가 아니며 "" error: too few template-parameter-lists" 에러를 발생 한다.

아래와 같이 수정하도록 한다 :
 template<> int A<int>::a = 0;
 template<> const char * const  A<int>::name = "example";


Example code 2

 #include <iostream>
 template <int I>
 class B
 {
 public:
   static int b;
 };

template<> int B<1>::b = 1; template <int I> int B<I>::b = 0;

int main() { std::cout << B<1>::b << std::endl; // 1 std::cout << B<2>::b << std::endl; // 0 return 0;
} 
ref.
Too Few Template Parameter Lists : http://www.c2.com/cgi/wiki?TooFewTemplateParameterLists
Posted by kukuta

댓글을 달아 주세요

에러 객체란?

" 에러객체 = 에러코드 + 에러문자열 "

프로그래밍을 하다보면 종종 에러코드를 정의해서 써야하는 경우가 있다. 종종이라기보다는 항상 에러코드를 정의해야한다. C++을 사용하는 나의 경우에는 enum을 주로 이용하여 에러코드가 겹치는 것을 방지하고, 다른 언어를 사용하는 사람들도 각자의 언어에서 제공하는 여러가지 방법을 사용하여 에러 코드를 정의해 쓸 것이다. 이렇게 에러코드들을 주루룩 정의해서 쓰다보면 한가지 아쉬운 점이 꼭 생각난다.

"에러 코드만 넣으면 무슨 에러인지 알수 있는 방법은 없을까?"

오늘 소개해볼 '에러 객체'라는 것은 위의 불편함을 조금이라도 해결해 보기 위해 만들어진 몸부림 중에 하나다.

에러 객체에게 필요한 것들?

중복은 절대 안되!!
에러 코드는 중복되어서는 안된다. 한 가지의 에러를 나타내는데 하나의 코드만 쓰여야하고, 하나의 코드는 하나의 에러만을 가리켜야한다. 그래서 사람들은 enum 같은 곳에 에러 코드들을 정의함으로써 컴파일 타임에 자동적으로 중복되는 에러 코드를 피하려한다. 우리가 필요한 에러 객체에 중복된 코드가 있는 경우엔 컴파일 타임 에러가 발생할 수 있게 해야 한다.

코드만으로 무슨 에러인지 알고 싶어요!!
MS의 에러 코드를 생각 해 보라. 당최 알 수 없는 에러 코드가 떨어진다. 그 코드가 무엇인지 알기위해서는 MSDN을 뒤져 보거나 Error Lookup 툴을 이용하여야만 한다. 물론, 범용성을 제공하기 위해서는 정수형태의 에러코드 말고 다른 형태의 뭔가를 지원한다는 것이 상당히 어렵다. 하지만 나는 내 프로젝트에서 사용할 코드들을 정의하는 것이다. 문자열이 한글이든, 영어든 한가지만 선택하면 되는 것이고, 내가 알 수 있는 내용을 써 넣으면 된다.
지금 나에게 필요한 것은 범용성 보다는 별다른 툴 없이 바로 에러 문자열을 출력할 수 있는 방법이다.

에러 코드와 에러 문자열을 한자리에서 정의 하고 싶어요!!
만일 enum으로 에러 코드를 정의하고 문자열 배열로 에러 문자열들을 정의 하는 것도 괜찮은 방법이라 생각한다. 다만 에러 정의 들이 늘어나면 늘어 날 수록 유지 보수가 힘들어진다(진짜다 직접 해봐라!! 500개가 넘는 문자열 배열 과 에러코드를 동시에 관리하면 머리에 쥐난다).

♥ 어떻게 생겼을까?
위의 요구사항을 토대로 만들어진 코드를 살펴 보자. 장황한 서두와는 달리 실상 코드는 너무나도 간단하게 만들 수 있다.

에러 객체 기본 :
enum XXX_ERRORTYPE
{
     XXX_ERRORTYPE_SUCCESS, 
     XXX_ERRORTYPE_INFO,  
     XXX_ERRORTYPE_WARN,  
     XXX_ERRORTYPE_ERROR  
};

template <int TYPE, int SERVER, int ERRNO> 
struct XXX_ERROR {
     enum {
          ErrorCode = ((TYPE << 8 * 3) | (SERVER << 8 * 2) | ERRNO)
     };
     static const char* Desc;
};

#define XXX_MAKE_ERRORCODE(name, type, server, errno, description) \
const char* XXX_ERROR<type, server, errno>::Desc = #description; \
typedef XXX_ERROR<type, server, errno> name;

// example
XXX_MAKE_ERRORCODE(XXX_ERROR_SOMEERROR,   TZ_ERRORTYPE_ERROR, 1,  1, "XXX 에러.")

그냥 ERROR이라고 하면 다른 뭔가와 충돌이 날것 같아 앞에 XXX를 붙였다. XXX에 큰 의미를 두지는 말자.

먼저 XXX_ERROR 객체를 살펴 보자.
ErrorCode라는 enum을 선언하고 있다. ErrorCode는 값이 아닌 하나의 타입으로 인식되어 컴파일 타임에 결정 된다. TYPE, SERVER, ERRNO 같은게 나오고 이리 저리 시프트가 있어서 복잡해 보이기는 하지만 그것은 개인적으로 에러 코드를 카테고라이징하기 위해 사용하는 것이지 특별한 의미가 있는 것은 아니다. 필요 없다면 ERRNO 하나만으로 써도 무방하다.

Desc는 description의 약자로 static const* 타입으로 선언되어 있다. 꼴에 문자열인지라 아무래도 컴파일 타임에 뭔가를 할 수 없어 대신 런타임에 아무도 내용을 바꾸지 못하도록 const를 사용했다.

XXX_MAKE_ERRORCODE 매크로로 눈을 돌려 보자. 이 매크로는 XXX_ERROR 템플릿 클래스를 찍어내는데 사용된다. 내부에는 static 변수의 초기화와 typedef 외에 다른 일은 하지 않는다. 하지만 한가지 기억할 것이 있다.

"템플릿 클래스는 인자가 다른 경우 다른 타입으로 여겨진다."

위에서 type, server, errno에 다른 경우 동일한 템플릿 클래스로 만들고 있지만 항상 다른 타입으로 여겨진다는 것이다. 만일 위의 세 인자가 같은 경우(혹 하나로 해도 상관은 없다)라면 동일 타입의 static 변수를 두번 초기화하려 한다고 컴파일 에러가 발생한다. 만일 name이 같은 경우 역시 typedef를 두번 하려한다고 컴파일 에러를 발생 시킨다. 어쨋든 중복된 선언을 하게 된다면 컴파일이 안되게 만들었다는 말이다.

사용은 어떻게?
에러 객체라고 하지만 실상 아무런 객체도 생성되지 않는다. 그냥 타입의 enum 상수와 static 변수에 접근하는 것 뿐이다 :

if(XXX_ERROR_SOMEERROR::ErrorCode == SomeFunction())
{
     std::cout << XXX_ERROR_SOMEERROR::Desc << std::endl;
}

위에서 사용된 SomeFunction()은 에러 객체 템플릿 클래스에 대한 어떠한 정보도 없어도 된다. 단순히 정수형태의 에러 코드를 리턴하는 것이면 된다. 리턴하는 것은 에러 객체가 아닌 에러 객체에 정의된 enum을 리턴 한다.

문자열의 출력 역시, 에러 객체에 선언된 static 변수에 접근하여 초기화된 문자열을 출력하는 간단한 형태다.

위와 같은 방식으로 에러 정보를 관리하게 되면 에러 코드 넘버와 에러문자열을 항상 한쌍으로 관리 할 수 있으므로 관리에 용이성과, 컴파일 타임에 중복 체크를 보장 받을 수 있다.

다만 단점으로는 static 변수들의 무수한 생성과 enum 처럼 자동으로 에러 코드를 증가 시켜주는 도구가 없다는 불편함이 있다.



참조 :
 - Modern C++ Design - 안드레 알렉산드레쿠스
 - [진리는어디에] - 템플릿 특화를 이용한 튜플 클래스
 - [진리는어디에] - 템플릿 특화(Template specialize)
 - [진리는어디에] - 템플릿 특화를 이용한 멀티 키 맵(Multi Key Map)
Posted by kukuta

댓글을 달아 주세요

이전에 템플릿 특화(template specialization)특화를 이용한 튜플(Tuple)클래스를 만들어 본적이 있다. 오늘은 비슷한 원리를 이용해 N개의 키를 가질 수 있는 멀티 키 맵을 만들어 보겠다.


멀티키 맵이라고 해서 특별이 다를 것은 없다. 다만 단일 키만을 제공하는 std::map을 좀 더 확장하여 N개의 키를 타입 리스트를 통해 넘겨주는 것 뿐이다. 물론 이런 방법 말고도 구조체를 키로 쓰는 std::map을 활용하거나, 바이너리 메모리로 모든 키들을 복사해서 쓰는등 구현에는 다향한 취향과 트레이드 오프가 존재한다. 오늘 이 포스트에서 소개하는 것은 그 방법들 중 한 가지 방법일 뿐이다.

멀티키 맵에 관련된 설명에 앞서 이전에 다뤘던 Typelist라던지 템플릿 특화의 개념은 이미 이전 포스트에서 익혔다고 가정하고 간단하게 설명하거나 다루지 않고 넘어 가도록 하겠다.

1. 기본 개념은 '상속' - 상속을 통한 코드의 자동 생성

이전의 튜플(Tuple)클래스와 마찬가지로 멀티키 맵도 기본 뼈대는 상속을 통해 만들어 진다. 멀티키 맵의 기본 구조를 보자면 각 키 마다 하위 계층 맵을가지고 있고, 가장 하위 키는 따로 맵을 가지지 않고 바로 값을 가지고 있는 형태다. 한 마디로 계층 구조를 가지는 std::map들을 템플릿을 이용해 자동으로 생성하는 방식이다.

1 : template <class T, class U, class Value>
2 : struct MultiKeyMap<Typelist<T, U>, Value> : private MultiKeyMap<U, Value>
3 : {
4 :    typedef Typelist<T, U> TYPELIST;
5 :    typedef std::map<T, MultiKeyMap<U, Value> > CONTAINER;
6 :    CONTAINER m_mapContainer;
...     .....

위 코드 라인 2번을 보면 여러 개의 키중 한개 키만을 떼어 자식 클래스 내부 std::map의 키 값으로 쓰고 나머지 타입 리스트는 부모 클래스로 올려 버린다. Value는 최상위 클래스의 값으로 저장되어야 하므로 모든 계층구조를 통해 끝까지 올려 보낸다.

위와 같은 상속을 통해 각 키에 해당하는 std::map을 자동으로 생성할 수 있다(이렇게 하지 않았다면 직접 각 계층 구조 마다 std::map을 생성 해 줘야 하는 불편함이 있었을 것이다.)

코드 라인 5번은 멀티키 맵 내부에 값을 저장 할 std::map을 생성한다. 위 코드를 보면 어떻게 계층 구조를 이루는 맵을 생성하는지 알 수 있다.

2. 멀티 키 맵에서의 데이터 접근
데이터의 접근은 위의 개념보다 좀 더 쉽다. 단순히 std::map의 계층 구조를 따라 다 맨 마지막 실제 값에 다다르면 값을 리턴하는 방식이다.

template <class Value>
struct MultiKeyMap<NullType, Value>
{
    Value m_value;
    Value& get(NullType key)
    {
        return m_value;
    }
};

template <class T, class U, class Value>
struct MultiKeyMap<Typelist<T, U>, Value> : private MultiKeyMap<U, Value>
{
    ....
    template <class PList>
    Value& get(PList key)
    {
         MultiKeyMap<U, Value>& mkm = m_mapContainer[key.first];
         return mkm.get(key.second);
    }
    ....
};


3. 멀티 키 맵의 사용
int main() {
    MultiKeyMap<TYPELIST_3(std::string, std::string, int), int> multiKeyMap;
  
    multiKeyMap[PARAMLIST_3("key1", "key2", 0)] = 1000;;
    std::cout << multiKeyMap[PARAMLIST_3("key1", "key2", 0)] << std::endl;
    multiKeyMap.erase(PARAMLIST_3("key1", "key2", 0));

    return 0;
}

멀티 키 맵의 템플릿 인자를 다양화 하기 위해서 Modern C++ Design에서 소개 되고 있는 타입리스트(type list) 를 이용한다. 그리고 타입 리스트에 따라 실제 인자도 다양화하기 위해 PARAMLIST 를 사용하고 있다. 타입리스트와 파라메터 리스트의 개념은 특화를 이용한 튜플(Tuple)클래스 를 참고하면 되겠다.

나머지 상세 코드는 첨부 파일을 참조 바란다.
Posted by kukuta

댓글을 달아 주세요

1. 왜 윈도우(Win32) 프로그래밍에서 콘솔 창이 필요한가

윈도우 창에서 프로그램을 작성하다 보면 쉽게 쉽게 디버그 상태를 보여 줄수 있는 창이 있다면 좋을 텐데 라는 생각을 종종 하게 된다. 물론 새로운 자식윈도우를 하나 더 띄우고 거기에 리스트 컨트롤을 붙인다면 그런 문제가 해결 되긴 하겠지만 그리 크지도, 작도 않은 프로그램에서 새로운 디버그 창을 만들어 내는 것은 여간 귀찮은 일이 아니다.

여러 SendMessage등 여러 단계를 거쳐야 하고, 초보에게 익숙하지 않은 함수를 쓰는 것보다는 printf를 사용한다면 프로그램 작성의 편리와 가독성을 높일 수 있을 것이다.

2. 사용 방법

 1) #include <stdio.h>
  콘솔 모드에서의 입출력을 하기위해서는 'stdio.h'가 필요하다.

 2) 본 문
  다음 두 함수로 콘솔 창을 띄울수 있다. 하지만 printf 함수를 호출하기 전까지는 창이 뜨진 않는다.

    AllocConsole();
    freopen( "CONOUT$",  "wt", stdout); 
    //CONOUT$ - console 창
    //wt - 텍스트 쓰기 모드
    //stdout - 출력될 파일 포인터(모니터로 지정)

    printf("hello DEBUG\n");

 위의 구문을 WinMain에 추가하면 콘솔창이 뜬다.


Posted by kukuta
TAG C/C++, console

댓글을 달아 주세요

매번 까먹는다.

long long x = 9223372036854775807;
printf("%I64d\n", x);

unsigned long long ux = 18446744073709551615;
printf("%I64u\n", ux);

한가지 주의 할 점은 소문자 l(엘)이 아니라 대문자 I(아이)라는 것이다.
Posted by kukuta
TAG C/C++, printf

댓글을 달아 주세요

  1. 설탕꽃 2010.06.16 10:20  댓글주소  수정/삭제  댓글쓰기

    코드는 글자체를 바꿔서 올려주는 센스를 보여봐~ ㅎㅎ
    Il -> 자~ 이거 두개 뭐가 틀리게~

  2. 백승욱-김정애 2010.06.29 17:58  댓글주소  수정/삭제  댓글쓰기

    python 정규 표현식으로 검색했더니
    니 블로그가 나왔네...ㅋㅋ
    무려 가장 위에 ^^
    하이튼 그렇다고 ^^
    집도 가까운데 함 보자꾸나...
    이글 보면 바로 문자 보내삼.. 나 말고 정애 폰으루다가..
    백숙 해줄끼야 ㅋ