나머지 연산자에 의한 실수

아래와 같은 코드를 만들었다.

예로 들기 위해서 만든 것이라 별 의미는 없는 struct인데, time_stamp라는 값이 들어 왔을 때 이 값을 짝수이면 내부 변수에 -1을, 홀수이면 +1을 대입하도록 하였다. 그리고 다음과 같이 짝수는 2로 나눈 나머지가 0이라는 것을 이용해 switch - case 문을 만들었다.

struct Step
{
explicit Step(int time_stamp)
{
// 어떤 값을 2로 나누었을 때,
// 나머지 값은 0 또는 1만 가능하다.
switch (time_stamp % 2)
{
case 0: m_step = -1; break;
case 1: m_step = +1; break;
}
}
int m_step;
private:
Step();
};

이런 방식의 코드를 Test case를 만들기 위한 유틸리티로 집어 넣었고, 곧바로 정적 코드 분석 툴에 의해 위의 코드는 잠재적인 문제가 있다는 통보를 받았다. 지금 보면 당연한 것인데도 보고를 받고 문제를 알아차리는 데는 십 분 정도 걸렸다.


정답은, 특정 조건에서는 생성자에서 m_step에 값을 대입하지 않아서 가비지 값으로 남아 있을 수 있다는 것이고, 그것에 대한 직접 적인 원인은 time_stamp 변수가 음수일 때를 고려 하지 않았기 때문이다.

0 이하일 때 나머지 계산의 값을 보면,

time_stamp = 0, -2, -4, -6, -8, … 일 때는 0
time_stamp = -1, -3, -5, -7, -9, … 일 때는 -1

의 값이 된다. 따라서 이 경우는 위의 코드에서 case -1: 을 추가 해야만 정적 분석 툴에서 문제를 통보하지 않는다.


Posted by 슴갈

2011/08/07 21:12 2011/08/07 21:12
Response
No Trackback , No Comment
RSS :
http://avej.com/textcube/rss/response/34

Trackback URL : http://avej.com/textcube/trackback/34

Leave a comment
[로그인][오픈아이디란?]

switch 문의 최적화

내가 하는 일 중에는 API를 만드는 일도 한다. 그러다 보면 return type이 스펙에 맞게 제대로 되었나를 알아 보기 위해 negative test에 대한 test case를 만든다.

그 중에 enum의 경우는 사용자가 악의를 가지고 범위에서 벗어난 파라미터를 줄 수가 있으므로 그것에 관해서도 체크를 해야 한다. 예를 들어, A와 B만들 가지는 enum에 대해 강제로 음수나 아주 큰 양수를 보내는 행위 등을 막기 위함이다.

그래서 enum에 대해 허용된 값만을 넘겼는지는 체크하기 위한 다음의 매크로를 만들었다.

// 2개의 field를 가지는 enum 타입에 대한 값 체크
#define CHECK_INVALID_ENUM_X2(source, case1, case2) \
switch (source) \
{ \
case case1: \
case case2: \
break; \
default: \
return false; \
}

// 3개의 field를 가지는 enum 타입에 대한 값 체크
#define CHECK_INVALID_ENUM_X3(source, case1, case2, case3) \
switch (source) \
{ \
case case1: \
case case2: \
case case3: \
break; \
default: \
return false; \
}

2개 또는 3개의 값만 가지는 emul에 대해 주어진 파라미터의 값이 허용된 값 안에 있는지를 알아 보고, 그렇지 않을 때는 false를 리턴하게 되어 있다. (실제로는 last error나 error log 등을 세팅하기 때문에 더 복잡하지만...)

사실 이 macro는 특별한 문제가 없다. 다만 앞으로 이야기할 아주 특수한 경우를 빼고 말이다.

문제의 상황을 만들어 보기 위해 간단한 enum 2개와 API를 만들어 보았다.

enum EA { EA_1, EA_2 };
enum EB { EB_1, EB_2, EB_3 };

bool SetSomething(EA a, EB b)
{
CHECK_INVALID_ENUM_X2(a, EA_1, EA_2);
CHECK_INVALID_ENUM_X3(b, EB_1, EB_2, EB_3);

// SetSomethingInternal(a, b);

return true;
}

유효한 값이 2개가 있는 EA라는 enum과 유효한 값이 3개가 있는 EB라는 enum이 있고, SetSomething()이라는 API는 제일 먼저 a와 b라는 이름으로 들어 온 파라미터의 유효성을 체크한다.

그리고 나는 이 매커니즘이 제대로 작동하는 가를 확인 하기 위해 다음과 같은 test case를 만들었다.

#include <stdio.h>

int main()
{
bool r1 = SetSomething(EA(-1), EB_1);
bool r2 = SetSomething(EA_1, EB(-1));

printf("r1 = %d, r2 = %d\n", r1, r2);
return 0;
}

아마도 쉽게 구할 수 있는 유명한 컴파일러에서는 대부분 r1 = 0, r2 = 0 이라는 결과를 돌려 받게 될 것이다. 하지만 조금 이름이 알려진 어떤 회사의 embedded compiler에서는 이 결과가 r1 = 1, r2 = 0 과 같이 나왔다. 다양한 컴파일러에서 작업해야 하는 일이 하나의 업이 된 지금, 이렇게 동작이 다른 컴파일러를 보는 것은 그리 이상한 일은 아니다. 다만 문제를 분석해서 아무런 문제가 없는 코드로 만드는 것이 조금 귀찮은 일일 뿐이다.


Test case가 특정 기기에서는 일부 fail이 나온다는 보고를 받고는  바로 디버깅 작업을 시작하였다. 그리고는 놀라운(?) 사실을 알아 내었다. 일부 컴파일러에서는 최적화 옵션에 따라서는 CHECK_INVALID_ENUM_X2() 매크로 자체가 아예 코드에서 빠진다는 것이다.

유효 값이 2개 밖에 없는 enum에 대해 그것을 switch 문으로 분기를 하면, 내부적으로 if - else 문으로 바꾸게 되고, 이때는 if 쪽이나 else 쪽이나 코드가 break 밖에 없기 때문에 코드를 삭제하는 만행(?)을 저지른 것이다. 다만, emul이 3개가 있는 경우는 if - else 로 바뀌지 않기 때문에 위와 같은 최적화의 대상이 아니라 제대로 동작하는 것이다.

이런 경우는 직접 부딪쳐 경우를 파악할 수 밖에는 없는 것이다. 그리고 최대한 에러 체크를 하기 위한 부분은 최적화에 안 걸리는 쪽의 코드를 짤 수 밖에 없다. 위의 경우도 MACRO를 if (!(A and B)) 문으로 치환하여 해결하였다.

Posted by 슴갈

2011/06/04 19:50 2011/06/04 19:50
Response
No Trackback , a comment
RSS :
http://avej.com/textcube/rss/response/30

Trackback URL : http://avej.com/textcube/trackback/30

Comments List

  1. 슴갈 2011/08/24 15:34 # M/D Reply Permalink

    회사 사람들이 찾아준 해법, (http://gcc.gnu.org/bugzilla/show_bug.cgi?id=44328) -fno-tree-switch-conversion 를 사용하면 된다고 한다.

Leave a comment
[로그인][오픈아이디란?]
최근에 WIZ의 후속 기기인 CAANOO가 생긴 덕에, CAANOO의 툴체인에 익숙해져 보자는 미명으로 WIZ로 만들었던 게임을 포팅하고 있다.

처음부터 SDL 등으로 했으면 좋았을 텐데, 개인적인 만족을 위해 굳이 GPIO를 쓰고자 했던 지라 지금 CAANOO로 포팅하는 데 많은 애를 먹고 있다.

CAANOO 의 포팅 이야기는 다음에 하고 이번에 40분 이상을 고생하게 만든 코드를 소개하고자 한다. (printf()라도 되면 금방 잡았겠지만, 실행하면 그냥 기기가 다운되는 디버깅 환경이었다)

문제의 발단은

int num_chunk = read(fd, &data, sizeof(data));
if (num_chunk < 0)
return;
num_chunk /= sizeof(data[0]);

라고 처음에 만들었던 코드를 다음과 같이 최적화를 한답시고 한 줄 줄인 것이 원인이었다.

int num_chunk = read(fd, &data, sizeof(data))
                / sizeof(data[0]);
if (num_chunk < 0)
return;

man page에 의하면 read()가 size_t를 리턴하니, 같은 리턴 타입인 sizeof()로 나누어도 문제가 없을 것이란 예상이었다. 그와는 또 다르게 read()가 실패하면 -1을 돌리는 함수라는 것을 알고 있었고, (음수 / 양수)가 되면 결과는 음수가 되리라는 기본 산수적인 생각도 했기에 위의 코드는 문제가 없다고 생각하고 넘어 갔던 것이다.

나눗셈을 하는 순서가 조금 다를 뿐이었지만 이 부분이 잘 못되었다고 생각하기가 어려웠기에 이 주위에서 printf()를 찍으면서 좀 많이 헤매었다.


결론적으로, 내가 man page에서 read()가 size_t를 넘긴다고 보았던 것이 나의 착각이었다. 실제 함수의 프로토타입은 다음과 같다.

ssize_t read(int fd, void *buf, size_t count);

size_t가 아니라 signed인 ssize_t였다. 결국은 signed int / unsigned int 이고, 이 경우는 강제로 앞 쪽의 signed int가 강제로 unsigned 로 캐스팅되어서 결코 음의 결과 값이 나올 수가 없게 된다. 6~7년 전에도 회사 코드에서 비슷한 문제가 있어서 실수한 적이 있는데 또 그 상황이 재현되었다.

이것과 완전히 같은 내용은 아니지만 signed 와 unsigned의 비교 방법도 참 특이(?)한데, 다음과 같이 비교가 진행된다는 것을 꼭 알아야 한다.

- 양쪽 타입 크기가 같으면 signed 쪽를 unsigned로 변환
- 양쪽 타입의 크기가 다르면 작은 쪽을 큰 타입으로 바꾼 뒤
  unsigned 쪽을 signed 로 변환하여 계산


그리고 여기서 재미 있는 것 한 가지...

#include <stdio.h>

int main()
{
{
int          a = -10;
unsigned int b = 10;

printf("-10 / 10 = %d\n", a / b);
}

{
short          a = -10;
unsigned short b = 10;

printf("-10 / 10 = %d\n", a / b);
}

return 0;
}

이것을 실행 시키면 어떻게 될까. 결과는 다음과 같다.

-10 / 10 = 429496728 /* int / unsigned int의 결과 */
-10 / 10 = -1 /* short / unsigned short의 결과. */

방금 말한대로라면 계산이 이상해져야 정상인데, 왜 short 일 때는 잘 되는 것일까? 왜 이런지 이유를 알고 싶으면 assembly로 결과를 보내어서 보면 알 수 있다.

Posted by 슴갈

2011/05/29 17:06 2011/05/29 17:06
Response
No Trackback , No Comment
RSS :
http://avej.com/textcube/rss/response/29

Trackback URL : http://avej.com/textcube/trackback/29

Leave a comment
[로그인][오픈아이디란?]

this에 대한 NULL 체크

나는 플랫폼의 API는 C++이 아닌 C가 되어야 한다고 주장한다. 하지만 어떠한 이유에서든 현재는 Open API가 C++인 플랫폼을 개발하고 있다.
 
Open API가 C++ 이다 보니 여러 가지 문제가 많긴 한데(장점도 있겠지만) 그 중에 하나는 NULL instance가 그 객체의 method를 부르려고 할 때 방어 하는 방법이다.

최종적으로 Open API의 제공자는 모든 경우의 사용자의 오남용을 막을 의무가 있기에 관련 method마다 제일 앞에 다음과 같은 코드를 넣고 있다.
 
void MyClass::SetValue(int value)
{
        if (this) // (1)
        {
               this->m_value = value;
        }
}
 
이렇게 했을 때, 다음과 같은 실수를 막을 수 있을 것이란 기대이다.
 
MyClass* pMyClass = CreateMyClass(…);
// NULL 체크를 하지 않았음
pMyClass->SetValue(100);
 
물론 생각대로 잘 동작하고 있었고 국내 모 전자 회사의 TV에는 이런 코드로 실제 애플리케이션의 실수를 막고 있다. –MIPS와 ARM을 사용-
 
하지만 문제는 휴대폰에서였는데, 여기에서는 이 경우 (1)이라고 표시한 부분에서 항상 프로그램이 죽는다 (죽는 이유는 좀 복잡하지만 그건 생략). 여기서 사용한 컴파일러는 ARM core용이긴 하지만 armcc와는 조금 다른 다른 부류이다. 일단 나는 표준 C++의 동작을 따르지 않는다는 이유로 컴파일러 버그로 통보해야 한다는 주장을 했지만, 담당자가 알아 보더니 C++ 스펙에서는 this가 NULL일 경우에 대한 참조는 보장할 수 없다는 것이 답이라고 한다.
 
C++을 사용할 때, virtual table의 구조라든지 method call과 관련된 구조를 숙지하게 되고 그 내용에서는 보통 this는 stack이나 register를 통해 전달 되기 때문에 위의 코드는 표준에서도 문제가 없다고 생각하기 마련인데 사실은 그렇지 않나 보다.


Posted by 슴갈

2010/12/29 23:18 2010/12/29 23:18
Response
No Trackback , 9 Comments
RSS :
http://avej.com/textcube/rss/response/28

Trackback URL : http://avej.com/textcube/trackback/28

Comments List

  1. ptptomr 2010/12/30 10:24 # M/D Reply Permalink

    오옷 오랫만의 posting이군요. 예를들어 주신 것과 같이, 빈번한 사용자 실수가 예상되는 case (missing of checking null pointer) 에 대해 platform이 방어할 수 있는 방법이 없다니 믿기지 않습니다.

    1. 슴갈 2010/12/30 13:34 # M/D Permalink

      아예 안드로이드처럼 VM을 도입하든지, 아니면 this 자체를 참조할 수 있도록 해주는 대부분의 컴파일러를 사용하면 문제는 없을 것 같습니다. 다만 표준이 아니라는 것일뿐, 대부분의 컴파일러는 위의 코드로 제대로 방어해 주잖아요.. ^_^

  2. doyongid 2011/01/19 08:30 # M/D Reply Permalink

    와우. 좋은 내용 감사합니당

  3. summerlight 2011/01/25 11:10 # M/D Reply Permalink

    가상 함수를 통한 간접 분기에서 this가 null이거나 해당 타입의 객체가 아니라면 적절한 vtable을 참조할 수 없어서 충돌이 일어납니다. 뭐 그렇다고 해도 거의 모든 컴파일러가 정적으로 결정되는 함수 호출에 대해서는 안전한 코드를 만들긴 하지만요.

    표준에서 이렇게 어디에서는 허용하고 어디에서는 안 하는 식의 예외를 두면 혼란스럽기 때문에 this == null인 경우는 일관되게 정의되지 않은 동작을 하도록 한 것으로 알고 있습니다.

    1. 슴갈 2011/01/30 15:31 # M/D Permalink

      virtual 함수이면 이미 진입 자체가 안될거고요.. NVI라면 진입이 될 것이며 그때 검사하려고 한 것이지만, 결과는 undefined이네요 -_-;; 문제의 경우는 RVCT에서 어떤 옵션(파악은 안 된)의 조합에서만 발생했습니다. 전혀 엉뚱한 주소에서 죽는 것으로 보아서는 객체를 관리/저장하는 방식이 좀 다른 것이 아닌가까지만 추측을 해보았습니다.

  4. summerlight 2011/02/08 12:13 # M/D Reply Permalink

    혹시 컴파일러의 명령어 재배치나 혹은 CPU의 비순차 실행 때문에 발생하는 오류 아닐까요? this가 항상 유효하다고 가정하면 수행 효율 증대를 위해 if (this) 내부의 구문부터 투기적으로 수행을 시켜볼 수도 있는데, 여기에서 this가 NULL이라 미정의라능! 하면서 배를 째는 걸지도 모르겠습니다. 저도 예전에 명령어 재배치로 인해 발생한 문제로 (이건 멀티 쓰레드 문제였지만) 고생했던 적이 있습니다. -_-;

    1. 슴갈 2011/03/09 22:07 # M/D Permalink

      ARM이 과연 그럴지는 의문인데요... 그 이후로도 가끔씩은 한 번 원인을 파악해 볼까하는 생각도 들었지만 업무에 치이다 보면 그걸 실행에 옮기기는 좀 힘드네요.

  5. 비밀방문자 2011/04/10 02:08 # M/D Reply Permalink

    관리자만 볼 수 있는 댓글입니다.

    1. 슴갈 2011/05/10 20:07 # M/D Permalink

      ooo님의 미래에 긍정적인 역할을 하게 되었다면 저도 영광입니다. ^^

Leave a comment
[로그인][오픈아이디란?]

C와 C++의 const 처리 방식

며칠 전 팀 사람들과 모여서 SDK 샘플에 사용할 코드를 리뷰 하다가 한가지 의문이 들었다. 헤더에서 const int로 선언한 상수의 instance는 어디에 생기느냐는 것이었다.

헤더라고 해도 결국 *.c / *.cpp 파일이 include하는 목적으로 사용되는 것이기 때문에 결국은 *.c / *.cpp에 직접 선언 한 것과 같아진다. , a.c b.c에서 동시에 const 선언된 상수가 있는 헤더 파일을 include 하게 되면 실제로는 같은 이름의 상수가 a.c b.c 에 동시에 선언이 된 것이다. 그리고 이것이 static이 아닌 이상은 같은 이름으로 선언된 것이 2개가 되므로 컴파일 에러를 내어야 한다.

일단 실험을 해보았다.


a.h

const int AAAA = 0;

a.c

#include "a.h"
int main()
{
    return AAAA;
}


b.c

#include "a.h"
int foo()
{
    return AAAA;
}

bash-3.00$ gcc a.c b.c
/cygdrive/c/ykahn/ccQerCRS.o:(.rodata+0x0): multiple definition of `AAAA'
/cygdrive/c/ykahn/cc3HVd2e.o:(.rodata+0x0): first defined here
collect2: ld returned 1 exit status



역시 예상대로 다중 선언에 의한 컴파일 에러가 발생했다.

그럼 여기서 드는 의문은, c++을 배울 때 꽤 앞 쪽에 나오는 이야기 중에는, ‘헤더에 쓰인 #define const로 대체하라라고 되어 있는데 그렇게 해도 문제가 없는 가였다. 기억으로는 c++에서는 헤더에 const를 넣고도 아무리 많은 cpp 파일에서 include를 해도 위와 같은 문제는 없었다는 것이 경험상의 결과였다.

그래서 이 번에는 cpp 파일로 테스트를 해보았다.


a.h

const int AAAA = 0;

a.cpp

#include "a.h"
int main()
{
    return AAAA;
}


b.cpp

#include "a.h"
int foo()
{
    return AAAA;
}


bash-3.00$ gcc a.cpp b.cpp
bash-3.00$ _

.. 그 동안 경험한 결과와 같이 이 경우에는 문제없이 컴파일 된다. 이걸 보고 가장 먼저 드는 생각은 일관성의 문제였다. 좋은 스펙은 예외 사항이 최소여야 한다고 생각되었기에 구글링을 통해 이 부분을 검색을 해보았다.

검색 결과 이 경우 c c++ const를 다르게 해석하며 ISO C++ 표준 Sec. 5.2.11.7, Sec. 7.1.5.1  그것이 정의되어 있다고 한다. , 스펙이 그렇다고 하니 더 이상 의문을 가질 필요는 없어졌고 각각의 상수에 대한 주소를 찍어 보면 include 한 개수만의 서로 다른 instance를 가지고 있는 것으로 나타난다.

Posted by 슴갈

2010/10/22 21:01 2010/10/22 21:01
Response
No Trackback , No Comment
RSS :
http://avej.com/textcube/rss/response/26

Trackback URL : http://avej.com/textcube/trackback/26

Leave a comment
[로그인][오픈아이디란?]

이상한 파라미터

조금 예전의 코드를 보다가 나중에 검토를 해 보자는 주석이 달린 코드를 보았다.
그리고 그 때 제시된 문제를 간략하게 만들어 보았다
.

#include <stdio.h>

class CRenderMode
{
public:
        explicit CRenderMode(int mode)
        {
               printf("mode = %d\n", mode);
        }
};

int main()
{
        int mode = 0;

        // 아래 것은 출력 안 됨.
        CRenderMode state0( int(mode) );

        // 나머지 3개는 출력 됨
        CRenderMode state_( int((int)mode) );
        CRenderMode state1( int(mode+0) );
        CRenderMode state2( (int)mode );

        return 0;
}

생성자에 파라미터를 넣을 때 int(mode) 라고 하니 원했던 생성자가 안 불렸다는 것인데, 마치 파라미터 없는 생성자를 부를 때 실수로 CRenderMode state0(); 라고 했을 때와 상황이 같다. 이것도 뭔가 스펙이 있는 듯 하지만, 정확한 이유는 모르겠다.

 

 

Posted by 슴갈

2010/04/25 12:45 2010/04/25 12:45
Response
No Trackback , 5 Comments
RSS :
http://avej.com/textcube/rss/response/24

Trackback URL : http://avej.com/textcube/trackback/24

Comments List

  1. 용맨소녀 2010/08/11 01:42 # M/D Reply Permalink

    explicit 가 뭐죠? ㅡ.ㅡ 플머 인생 18년동안 처음보는 단어군요.. (역시 헛살았다는..)

  2. 왕풍뎅이 2010/08/12 15:27 # M/D Reply Permalink

    http://bischoff.tistory.com/202 이거라는뎅 용만앙

  3. 슴갈 2010/08/31 12:36 # M/D Reply Permalink

    왕풍뎅이님 링크를 참조하시면 될 것 같고.... 잘 안쓰일 수도 있지만 꼭 필요한 것 중에 하나입니다. (저의 경우에는, 남이 쓰는 SDK를 만들어야 하는 직업이기에 중요하게 생각하는 키워드 이기도 하고요...)

  4. summerlight 2010/10/23 17:59 # M/D Reply Permalink

    오랜만에 들릅니다.

    위의 경우는 객체 instance의 선언이 아니라 함수 원형을 선언하는 것으로 간주되기 때문에 그렇습니다. 구체적으로는CRenderMode를 반환하고 int형의 인자를 받는 state0라는 함수를 선언한 셈이 되고요. 함수 선언과 객체 instance의 선언 사이의 구분이 서로 모호해서 발생하는 문제죠. C의 유산을 물려 받느라 어쩔 수 없이 발생한 일인 것 같습니다.

    그래서 저는 그냥 명시적으로 static_cast를 씁니다. 타이핑이 좀 길어지더라도 C++같이 위험한 언어에서는 최대한 모호함을 배제하는 것이 맞는 것 같습니다...

    1. 슴갈 2010/11/02 20:50 # M/D Permalink

      그렇군요. 간단하게 g++에서 확인을 해보았는데, 함수 선언의 파라미터로 int(mode) 와 같은 선언이 가능하군요. 처음 알았습니다. 감사합니다.

Leave a comment
[로그인][오픈아이디란?]

memset() 함수를 위한 헤더 파일은?

memset() 함수를 쓰기 위해 include 해야 하는 헤더 파일 때문에 종종 실수를 한다. 나는 아주 예전부터 <memory.h>를 사용해 왔고 대부분의 컴파일러에서는 문제를 일으키지 않았다.

 

내일 code release를 해야 하는 라이브러리가 있어서 여러 컴파일러에서 최종 문법 테스트를 한 후, 내가 라이선스를 가지지 못한 나머지 컴파일러에 대해서는 다른 분께 빌드를 의뢰했다. 그리고 의뢰 받은 쪽에서는 저 <memory.h>라는 것 때문에 오류가 난다는 통보를 해 줬다. 그래서 나는 즉시 <string.h>로 바꾸라고 메시지를 줬고 나머지 모두 빌드에 성공했다. <memory.h>를 못 찾을 때는 <string.h> include하면 된다는 것은 이전부터 알고 있었기 때문에 그렇게 대응한 것이지만 그때까지만 해도 그건 컴파일러의 잘 못이라고만 생각하고 있었다.

 

자리에 돌아와서 man page wikipedia에서 관련된 표준에 대해 조사를 해 보았다. 그런데 전부 다 표준은 <string.h>라고 나와 있었다. 매뉴얼을 제대로 보지 않은 내 잘 못이긴 하지만 대응되는 헤더가 그다지 직관적이지는 못하다는 생각도 동시에 든다.

Posted by 슴갈

2009/05/28 19:18 2009/05/28 19:18
Response
No Trackback , No Comment
RSS :
http://avej.com/textcube/rss/response/14

Trackback URL : http://avej.com/textcube/trackback/14

Leave a comment
[로그인][오픈아이디란?]

printf()의 출력이 이상하다?!

예전에 나와 같이 일하던 후임 중에 한 명이 어떤 문제를 가지고 왔다. 타겟 디바이스에서 디버깅 하려고 printf()를 사용했는데 아무래도 원하는 결과와는 다르게 나온다는 것이다. 결국 VC++에서도 동일한 문제가 나온다는 것을 확인해서 다음과 같이 문제를 간략화 시켰다.
코드
struct
{
int* a;
int* b;
int* c;
} a = {0, 0, (int*)1};

printf(
"%d, %d, %d\n", a.c, a, a.c);
결과
1, 0, 0
a.c와 a와 다시 a.c의 내용을 출력하는 예제이다. 분명 코드 상으로도 첫 번째 파라미터와 세 번째 파라미터는 모두 a.c로 동일하다. 그런데 결과는 1과 0으로 서로 다르게 나왔다.
아마도 쉽게 실수 할 수 있는 부분으로 생각되는데, 실수 하기는 쉬운데 반해 그 실수를 찾는데는 굉장히 힘들게 될지도 모르겠다.

Posted by 슴갈

2009/02/23 00:10 2009/02/23 00:10
Response
No Trackback , 3 Comments
RSS :
http://avej.com/textcube/rss/response/9

Trackback URL : http://avej.com/textcube/trackback/9

Comments List

  1. TJ 2009/04/03 08:33 # M/D Reply Permalink

    printf를 위해 스택에 데이터를 넣은 것과, printf에서 format string을 해석해서 꺼내는 데이터의 offset이 달라서 생기는 현상이죠?

    두번째 파라미터를 a.b나 a.a 등으로 바꾸면 마지막 값도 1로 나올 것 같은데요?
    printf한테 두번째도 숫자다.. 하고 %d를 줬지만, 실제 printf에게 전달되는 두번째 인자는 숫자가 아니라 struct a이기 때문에 발생하는 현상으로 보입니다.

    1. 슴갈 2009/04/08 22:31 # M/D Permalink

      좀 더 정확하게 이야기 하면, printf()에 3개의 인자를 넘기는 것처럼 보이지만 실제로는 5개의 인자를 보낸 것입니다. 그리고 %d는 3개 밖에 없는 것이고요.

  2. TJ 2009/04/09 09:36 # M/D Reply Permalink

    struct를 '인자'로 보지않고 primitive type만 '인자'로 생각한다면, 그렇게 생각할 수도 있겠네요.

Leave a comment
[로그인][오픈아이디란?]

Endian의 고려

자신이 영원히 MS Windows에서 코드를 만든다면 endian은 전혀 신경 쓰지 않아도 된다. 하지만 그렇게 만든 코드는 항상 little endian 전용이라는 꼬리표가 붙어 다녀야 제대로 된 것이다.

코드

unsigned long aaa   = 0x01FF0002;
unsigned char* pCh  = (unsigned char*)&aaa;
unsigned short* pWd = (unsigned short*)(pCh+1);
printf("+++++ %x, %x\n", *pCh, *pWd);

위의 코드는 endian이 전혀 고려되지 않은 코드이므로 일반적인 목적에서는 잘 못 만들어진 코드이다. 그래서 그 결과를 실제로 돌려보면 다음과 같다.

Little endian의 결과

+++++ 2, ff00

Big endian의 결과

+++++ 1, 00ff

Little endian의 결과 (ARM 컴파일러)

+++++ 2, 3d8f

Little endian과 big endian의 결과는 예상한 대로 서로 다르게 나타났다. 원래 메모리의 구조가 그런 것이니 이렇게 나오는 것이 맞다. 그렇기 때문에 위의 코드처럼 프로그램을 만들면 그 코드는 특정 endian에만 적용되는 코드가 되는 것이다.

그리고 여기서 말하고자 하는 것은 이것말고 또 하나가 있다. 제일 마지막에 나온 ARM 컴파일러의 little endian의 결과이다. 같은 little endian이라도 해도 chip-set이나 컴파일러가 달라지면 그 결과가 달라진다는 것을 말하기 위해서 마지막의 결과도 하나 추가했다.

ARM의 경우에는 3d8f라는 전혀 엉뚱한 값이 나타났는데 이 값은 그 결과를 예상할 수 없는 값이며 항상 바뀔 수 있다. 이런데 이 결과는 문제가 없다. 왜냐면 ARM 컴파일러의 매뉴얼에서는 이 문제에 대해서 확실히 언급을 하고 있기 때문이다. (각 자료형에 대한 align 문제이며 이것은 ARM의 동작과 관련된 문제이다) 그래서 비록 자신이 사용하는 컴파일러가 ARM용이 아니라고 해도 범용적인 코딩을 한다고 생각한다면 이런 것까지 다 고려해야 한다.

그래서 결론을 이야기 하자면, endian을 항상 고려해서 코딩을 해야 하며 모든 칩에서 호환이 가능한 문법으로 접근하자는 것이다.

Posted by 슴갈

2009/02/22 23:53 2009/02/22 23:53
Response
No Trackback , 4 Comments
RSS :
http://avej.com/textcube/rss/response/8

Trackback URL : http://avej.com/textcube/trackback/8

Comments List

  1. Mahavishnu 2009/03/06 18:01 # M/D Reply Permalink

    엔디안 문제에 대한 정답이 알고싶습니다.
    저도 범용 코딩 스타일을 지향하는지라 . .
    저런 경우에는 어떤 방식으로 구현하는 것이 좋은지요?

    1. 슴갈 2009/03/15 10:30 # M/D Permalink

      엔디안 문제의 정답은 '항상 원래 정의한 type만 사용하며, 그 type의 각각의 바이트를 제어하기 위해서는 shift와 masking을 이용한다'로 정의하면 되지 않을까 생각합니다. 위의 예의 경우에도 원래의 type인 unsigned long에서 masking으로 값을 뽑아야 하고요.

      단, 파일에 저장되는 경우에는 파일 포맷에 사용한 엔디안을 명시해야겠지요.

      little endian 명시 예: BMP 파일 등
      big endian 명시 예: PNG 파일 등
      둘 다 가능하지만 헤더에 명시: JPEG의 EXIF 등

  2. 유객주 2011/01/23 19:55 # M/D Reply Permalink

    big endian 결과가 +++++ 1, 00ff
    로 나왔는데,

    제 생각에는

    +++++ 1, ff00 이어야 할 것 같은데요.

    왜냐하면 big endian일 때 메모리 순서상 01FF0002로 저장되고 ff00 두 바이트를 big endian으로 읽으면 그 순서대로이므로, ff00로 나오지 않을까요?

    1. 슴갈 2011/01/30 15:21 # M/D Permalink

      예, 맞습니다. 0xFF00이네요. (글 쓸 때 big endian은 해당 타겟이 없어서 직접 안 돌려 보고 little endian 결과를 꺼꾸로 했더니... -_-;; )

Leave a comment
[로그인][오픈아이디란?]

g++의 버그

프로그램 개발자는 컴파일러를 의심하면 안된다. 제일 먼저 자신 탓을 하고, 하드웨어의 특성을 의심하고, 그것도 문제가 아니라면 그 때야 컴파일러를 의심해 보아야 한다.

그런데 수 많은 버그들과 씨름하다보면 가끔씩 컴파일러를 의심해야 할 때도 있다. 특히 변방의 열악한 컴파일러라면 그런 일이 좀 많긴하다. 그런데 이번에는 g++에 대해서 이야기 하고자 한다.

소스

#include <stdio.h>

int main()
{
unsigned char buffer[2] = {0, 0};
unsigned char* pByte = buffer;

*pByte = 1 + *pByte++;

printf("%d, %d\n", buffer[0], buffer[1]);

return 0;
}

이것의 결과는 어떨까. 나는 2개의 g++ 버전으로 테스트를 해보았다.

g++ 4.0.1의 결과

% g++ --version
g++ (GCC) 4.0.1 (Debian 4.0.1-2)
Copyright (C) 2005 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

% g++ q.cpp
% ./a.out
1, 0
% g++ q.cpp -O3
% ./a.out
1, 0

g++ 3.4.4의 결과

$ g++ --version
g++ (GCC) 3.4.4 (cygming special) (gdc 0.12, using dmd 0.125)
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ g++ q.cpp
$ ./a.exe
1, 0
$ g++ q.cpp -O3
$ ./a.exe
0, 1

확실히 뭔가 이상하다. 4번의 실행 결과는 모두 같아야 하는데 마지막 하나는 결과가 다르다. x86과 MIPS와 ARM의 컴파일러가 서로 결과가 다른 것은 그나마 자주 있는 일인데, 같은 x86인데도 불구하고  동일 컴파일러인데도 버전이 달라지면서 결과가 달라진다든지 최적화 옵션에 따라 결과가 달라진다든지 해서는 안될 것이다.

이 문제가 컴파일러를 만든 쪽에 보고가 된 것인지 아닌지는 알 수가 없지만, 하여간 나는 이 문제 때문에 며칠을 날려 먹었던 것이다. (x86 vs. ARM의 경우에는 컴파일러 특성으로 결과가 달라지는 것은 굉장히 흔한 경우이다. 다음 기회에 이것도 한 번 다루겠다.)

Posted by 슴갈

2009/02/22 23:22 2009/02/22 23:22
Response
No Trackback , 2 Comments
RSS :
http://avej.com/textcube/rss/response/7

Trackback URL : http://avej.com/textcube/trackback/7

Comments List

  1. gos 2009/12/26 07:31 # M/D Reply Permalink

    컴파일러 버그는 아닙니다.
    http://en.wikipedia.org/wiki/Sequence_point
    중간에 보시면
    i=i++가 예제로 있고 그 결과가 undefined인 설명이 나와 있습니다.
    (저는 g++가 변방의 열악한 컴파일러는 아니라고 생각합니다. ^^;)

    1. 슴갈 2009/12/28 09:29 # M/D Permalink

      오해가 있으셨나 본데, g++이 변방의 컴파일러라는 의미가 이니었습니다. -변방의 컴파일러에는 그런 문제가 많이 보이긴 한데, 이번에는 g++에 대한 이야기를 한다는 의미- 저는 현재 g++ 계열 이외에는 실무에 사용하는 컴파일러가 없을 정도입니다. (문법 체크를 위한 MSVC++ 정도가 더 추가...)

      그리고 글 내용에는 적지 않았지만 g++ 4.x 컴파일러가 나온 이후로는 저런 방식의 코딩은 warning을 내었습니다. 그리고 그 warning을 조사하다가 이 부분은 스펙상 undefined라는 것을 알고, g++의 각 버전에 대해 문제점을 체크 하던 중 위와 같은 내용을 발견하게 된 것이고요

      당시는 상당한 종류의 컴파일러(변방의 열악한 것을 포함한) 사용하고 있었는데 대부분은 undefined에 대해 내부적인 정책을 가지고 옵션이 변해도 1,0 또는 0,1의 동일한 결과를 유지했지만 cygwin의 g++ 3.4.4만은 옵션에 따라 내부적인 정책이 달라서 이 글을 쓰게 된 것입니다.

Leave a comment
[로그인][오픈아이디란?]

블로그 이미지

GP2X WIZ와 CAANNO와 bada용 게임 개발을 하자

- 슴갈

Notices

Archives

Authors

  1. 슴갈

Recent Trackbacks

Calendar

«   2012/02   »
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29      

Site Stats

Total hits:
36068
Today:
17
Yesterday:
44