현재 가장 많이 사용되는 컴퓨팅 시스템은 32bit 주소기반이지만, 향후 PC를 중심으로 64bit 으로 넘어갈 전망이다.

C언어의 경우 프로그램에서 사용되는 정수형을 살펴보면, short, int, long, long long 이 있는데 사용되는 시스템의 성격에 따라 32bit과 64bit에서 그 크기가 다를 수 있다.

예를 들어 int 형의 크기가 32bit 시스템에서는 32bit 으로 표현되지만, 64bit 시스템에서는 64bit으로 표현될 수도 있다. (ILP64)
이것은 32bit 시스템에서 잘 돌아가던 프로그램이 64bit에서는 오류를 일으킬 수 있다는 것인데, 프로그램 작성시 신경을 쓸 필요가 있다.

이러한 규칙을 정한 모델이 있는데 아래와 같다.


원래 int형은 그 시스템에서 가장 효율적이라고 추천하는 데이터 형이기 때문에 그 시스템의 word형으로 정한다.
따라서 64bit 시스템에서는 int형을 64bit으로 정하는 것이 이치이다. 하지만, 앞서 언급한 32bit과의 호환 문제 때문인지는 확실하지 않으나 Windows나 Unix 진형 모두 64bit 시스템에서도 int형을 32bit으로 유지하고 있다. 
int형과 달리 long형에서는 서로 다른 입장을 보이고 있다.

LP64진영의 주장은 프로그램에서 메모리 주소를 처리하는 변수를 int형으로 할 경우 64bit시스템의 주소를 모두 커버할 수 없어 주소를 담는 변수는 long형으로 하여 64bit시스템에서 문제가 없게 하자는 것이다. 개인적으로 이 주장은 설득력이 있다.

이러한 혼란을 막기 위해 프로그램시 int, long과 같이 정수형 예약어 그대로 사용하지 않고 tint32 또는 tint64 식의 재정의 하여 사용하는 기법이 주로 사용된다.

그러나, 잘 생각해 보면 primitive형을 사용해도 상관없다.  왜냐하면 ILP64나 SILP64는 거의 사용되지 않는 데이터모델이기 때문에 제외하고 LLP64와 LP64만 고려하면,  long형만 다른데 long형은 사용하지 않으면 된다.

메모리의 주소를 다룰때도 보통 포인터형을 사용하기 때문에 문제가 없다.

결론은 LP64, LLP64 고민하지 말고 프로그래밍시 int와 long long만 사용하도록 하자.

저작자 표시 비영리 변경 금지
신고
Write your message and submit
예전(2.4.x) 리눅스 커널의 double linked list 관련 소스를 보다보면 약간 당황스런 코드를 접하게 된다.

/**
 * list_entry - get the struct for this entry
 * @ptr:        the &struct list_head pointer.
 * @type:       the type of the struct this is embedded in.
 * @member:     the name of the list_struct within the struct.
 */
#define list_entry(ptr, type, member)     ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

이 매크로는 어떤 의미일까??  매크로를 보기 편하게 분해 해 보면.

(  (type *)   ( (char *)(ptr) - (unsigned long)(&((type *)0)->member))  )

빨간색에서 파란색을 뺀 값을 type의 포인터 형으로 형변환하는것을 알 수 있다. 좀 더 자세히 풀면,
char 포인터형인 ptr에서  시작주소가 0인 type의 맴버변수인 member의 주소를 unsigned long형태로 빼고 있다.

그림으로 살펴보면 아래와 같은데.


주소가 0인 type 구조체의 맴버변수인  member의 주소는 결국 type구조체에서 member의 offset을 의미한다.
종합해 보면 ptr은 type구조체의 member를 가리키고 있는데 여기에서 type 구조체내의 member의 offset을 뺀것을 의미한다.

결국 원하는 것은 member의 주소를 가지고 type구조체의 시작주소를 얻고 싶은것이다.
list_entry 구조체를 사용하면 member list에 엮여 있는 type구조체의 시작주소를 얻을 수 있고, 시작주소를 얻는다는 것은 type구조체의 모든 맴버변수에 접근할 수 있다는 것이다.

지금까지 어렵게 list_entry 매크로를 언급한 이유는 offetof 매크로를 설명하기 위함인데..

offsetof 는 표준 라이브러리 <stddef.h> 에서 제공하고 있는 매크로로 어떠한 맴버변수가 구조체안에서 어떤 위치에 있는 지 알려준다. offsetof와 sizeof연산자를 활용하면 어떤 구조체가 메모리에 배치되는 모양을 완벽하게 그려낼 수 있다.

사용법은 offsetof(구조체형, 맴버명) 을 입력하면 size_t형의 정수로 맴버의 구조체 상의 배치되어 있는 offset을 알려준다.

struct foo 
{
	char c;
	short int si;
	double d;
};


int main()
{
	printf("%lu\n", (unsigned long)sizeof(struct foo));
	printf("%lu\n", (unsigned long)sizeof(short int));
	printf("%lu\n", (unsigned long)offsetof(struct foo, si));
	printf("%lu\n", (unsigned long)sizeof(double));
	printf("%lu\n", (unsigned long)offsetof(struct foo, d));
	
	return 0;
}

위 예제의 결과는 임플리멘테이션에 따라 다르게 나올 것이다.

다시 list_struct로 돌아와서 offsetof연산자를 이용하면 위 매크로는 좀더 단순하게 바꿀 수 있다.

((type *)((char *)(ptr)-(unsigned long)(offsetof(struct type, member))))

ps. 실제로 offsetof는 아래와 같이 정의되어 있다.

#define offsetof(s,m)   (size_t)&(((s *)0)->m)



저작자 표시 비영리 변경 금지
신고
Write your message and submit
어떤 객체를 표현하는 여러 변수를 맴버로 가지고 있는 구조체가 있다고 가정하자.

이 구조체의 인스턴스를 캐슁 해야는 상황이라면 이 구조체가 클수록, 인스턴스가 많을 수록 메모리를 많이 필요로 하게 된다.

하지만, 구조체의 맴버 변수들이 아주 작은 범위의 값(char형 보다 작은)을 갖는다고 한다면 메모리가 아깝다는 생각이 든다. 이때 요긴하게 사용할 수 있는 기능이 비트필드이다.

선언 방법은 맴버를 일반적인 정수형으로 선언하되 콜론(:)과 비트필드의 크기를 적어주면 된다.

struct foo 
{
	unsigned int a:4;
	unsigned int b:5;
	unsigned int c:6;
};
위와같이 선언하면 비록 unsigned int로 선언되어 있지만, 각각 4, 5, 6bit씩 메모리에 할당되게되어 그만큼 메모리를 절약할 수 있다.

sizeof(struct foo) 를 출력해보면 4가 나오는 것을 확인 할 수 있다. 이것은 실제 사용하는 bit의 값이 4 + 5 + 6 = 15bit 이지만, 변수 타입이 unsigned int이기 때문에 그렇게 되는 것이다.

struct foo 
{
	unsigned short int a:4;
	unsigned short int b:5;
	unsigned short int c:6;
};
이 경우 sizeof(struct foo)의 값은 2가 된다. 사용법은 일반적인 구조체의 사용방법과 다르지 않지만, 변수 대입시 설정된 비트 이상은 표현하지 못한다.
struct foo 
{
	unsigned int a:4;
	unsigned int b:5;
	unsigned int c:6;
};


int main()
{
	struct foo o;

	o.a = 0xFF;
	o.b = 0xFF;
	o.c = 0xFF;
	
	printf("foo size : %d\n", sizeof(o));
	printf ("%08x, %08x, %08x\n", o.a, o.b, o.c);
	return 0;
}
프로그램의 결과는 다음과 같다. 

 foo size : 4

0000000f, 0000001f, 0000003f 

상황에 따라 나름대로 유용하게 사용될 수도 있는 기능이지만, 애석하게도 사용에 많은 제약이 있다. 

임플리멘테이션(보통 컴파일러라고 표현한다.)에 따라 비트의 메모리상 배치가 다를 수 있어 주의하지 않으면 환경에 따라 중대한 버그를 만들 수 있다.

이러한 이식성 문제 때문에 실제 사용되는 코드를 만나기란 쉬운일이 아니다.

C언어 표준이 좀더 일찍, 임플리멘테이션까지 규칙을 정했으면 좀더 재미있는 C언어가 되지 않았을까? 하는 생각이 든다.

저작자 표시 비영리 변경 금지
신고
Write your message and submit
[NSD][CRT][Tim:0][Err:00000][FileSetAttributes(#1286)@ns_file.c]: Native set attr (0) (0) fails. ERR[0]


SQA  도중에 위와 같은 메시지가 출력되었다는 것이었습니다.

[CRT] 레벨의 에러는 절대 발생해서는 안되는 에러이기 때문에 사뭇 심각했습니다. 물론 나중에 별 문제가 아니라고 밝혀 졌지만,

 진짜 문제는 ERR[0]이었습니다.

 문제의 코드를 보면
err = pVnode->pVnodeOps->pfnSetAttributes(pVnode, (dwAttribute & FILE_ATTR_MASK));
if (FERROR_NO_ERROR != err)
{
	NSD_EMZ((_T("Native set attr (%u) (%d) fails. ERR[%d]"), NsVnodeGetIndex(pVnode), dwAttribute, err));    break;
}


위 코드를 보면 err이 FERROR_NO_ERROR이 아닐 때 즉, err이 0이 아니어야만 if안으로 진입하여 메시지를 찍을 수 있는 것이었습니다.
 
"에러가 0인데 메시지가 출력되었다!"

어떻게 이런일이 발생 할 수 있을까요???

err가 0이 아니었기 때문에 if안으로 진입한 것인데 정작 err은 0이었다니... 송선임님이 안가르쳐 주셨다면 한참 고민했을 겁니다.
 
범인은 %u였습니다. NsVnodeGetIndex()의 리턴값은 unsigned long long형 이었습니다. 크기가 8byte라는 것이지요.
8byte의 변수를 4byte unsigned int 구분자인 %u를 사용하여 4byte만큼 찍고 나머지 4byte는 그대로 뒤로 밀려 버렸습니다.

그 결과 (%u)와 (%d)에 NsVnodeGetIndex()의 리턴값 8byte가 4byte씩 나누어 찍히고, ERR[%d]는 뒤 따라오는 dwAttribute가 찍힌 것이었습니다. (우연히도 NsVnodeGetIndex()와 dwAttribute의 값이 모두 0이었습니다.)

정작 err은 출력 구분자(%)가 없어 출력되지 못했습니다.

위 코드를 아래와 같이 고쳐 해결 하였습니다.
NSD_EMZ((_T("Native set attr (%llu) (%d) fails. ERR[%d]"), NsVnodeGetIndex(pVnode), dwAttribute, err));

printf의 출력 구분자와 변수의 type이 맞지 않아도 컴파일러는 경고나, 에러를 발생하지 않습니다.
 
ps 오늘의 교훈 : 에러 메시지 출력을 잘못해도 SQA를 통과 못할 수도 있습니다.


신고
Write your message and submit
struct _BUFFER_INFO
{    
    BUFFER_MANAGER                stBM;                
    pBuffer[1];           
}; 
_BUFFER_INFO는 buffer cache의 전체정보를 담고 있는 자료구조로 크게 entry들을 관리하는 BUFFER_MANAGER와 실제 data가 저장되는 pBuffer로 나뉘어 있습니다.
char pBuffer[1]를 유심히 살펴 보세요. 엥? 이건 무슨 코드인가요?
왜 char *pBuffer 라고 하지 않았을까요?

BUFFER_INFO는 buffer cache가 초기화 될 때 전체 메모리를 할당 받게 됩니다.
만약 char pBuffer[1]이 아닌 char* pBuffer였다면 BUFFER_INFO의 초기화는 이렇게 되었어야 합니다.
static PBUFFER_INFO            g_pBI;        
g_pBI = RtlAllocMem(sizeof(BUFFER_INFO));
IF (NULL == g_pBI)
{  
    NSD_CMZ((_T("g_pBI allocation fail, ERR[%d]"), err));   
    err = FERROR_INSUFFICIENT_MEMORY;   break;
}
g_pBI->pBuffer = RtlAllocMem(BCACHE_SIZE);
IF (NULL == g_pBI->pBuffer)
{   
    NSD_CMZ((_T("g_pBI->pBuffer allocation fail, ERR[%d]"), err));    
    RtlFreeMem(g_pBI);    
    g_pBI = NULL;    
    err = FERROR_INSUFFICIENT_MEMORY;    
    break;
} 

즉, BUFFER_INFO와 pBuffer의 메모리를 두번 할당 받아야 합니다. 물론 해제 할때도 마찬가지 입니다.

char pBuffer[1]이라면 어떻게 될까요?

static PBUFFER_INFO            g_pBI;    
g_pBI = RtlAllocMem(sizeof(BUFFER_INFO)+ BCACHE_SIZE);
IF (NULL == g_pBI)
{   
    NSD_CMZ((_T("g_pBI allocation fail, ERR[%d]"), err));    
    err = FERROR_INSUFFICIENT_MEMORY;    break; 
} 

메모리 할당이 한번으로 끝났습니다.
중요한점은 BUFFER_INFO와 pbuffer가 메모리상으로 한덩어리로 잡힌다는 것입니다

이러한 접근은 메모리 할당을 두번에서 한번으로 줄인것 말고도 좋은 점이 있습니다

만약 이 BUFFER_INFO를 Log와 같이 File에 주기적으로 저장을 한다고 하면 어떨까요? char *pBuffer 로 사용했을 경우는 이 자료구조에 두번 접근해야 합니다. 필요없는 memcpy도 수반될 수 있습니다. 또한, BUFFER_INFO 와 pBuffer가 메모리 상으로 떨어져 있기 때문에 CPU에서 cache miss가 발생하는 확률도 높아집니다

char pBuffer[1]

별거아닌거 같지만, 코드도 짦아지고, 수행시간도 짧아질 수 있어 좋은 테크닉인 것 같습니다.

char pBuffer[0]도 의미는 유효하지만, C90표준에서는 허용하지 않고 있습니다. 일부 컴파일러는 경고나 에러를 낼 수도 있습니다.


ps. 하지만, 가독성은 좀 떨어지네요. ㅎㅎ
ps2. C99 표준에서는 가변길이 배열을 허용하고 있습니다. 즉, char pBuffer[] 도 가능합니다.
신고
Write your message and submit
Endian에 관한 문제는 embedded system에서는 항상 따라다니는 요소입니다.
 
little-endian에서는 제대로 돌아가는 프로그램이 big-endian에서는 오류를 일으키거나 하는 일은 다반사죠.
그래서 두가지 endian을 지원하기 위해 똑같은 테스트를 반복해서 하는 경우도 있죠.

보통은 매크로를 사용하여 빌드시 endian을 정하기도 하지만, runtime시 정할 수도 있습니다.

다음의 코드는 실행환경의 endian을 알아내는 코드입니다. (소스 수정 없이 두가지 endian을 테스트 할 수 있음을 의미합니다.)

#include "stdio.h"

int main(void)
{
    union
    {
        unsigned long int i;
        unsigned char uc[sizeof(long int)];
    }u = {1};

    if(u.uc[0] == 1) printf("little-endian");
    else if(u.uc[sizeof(long int)-1] == 1) printf("big-endian");
    else printf("unknown endian??");

    return 0;
} 

union은 잘사용되지 않는 자료형이지만 유용하게 사용되었네요.
신고
Write your message and submit
아래 코드의 실행 결과는 어떻게 될까요?
#include <stdio.h>
#include <string.h>
int main(void)
{
       int a = strlen("123");
       int b = strlen("123\0");
       int c = strlen("123\012");
       int d = strlen("123\0123\0ABC");
       int e = strlen("123\0ABC");
       int f = strlen("123\0123");
       int g = strlen("123\0""123");
       printf("a:%d, b:%d, c:%d, d:%d, e:%d, f:%d, g:%d\n", a, b, c, d, e, f, g);
       return 0;
}
지난번 삼중자와 마찬가지로 결과를 예측하기 쉽지 않습니다.

힌트 : vim에서 위 코드를 열어보세요.
신고
Write your message and submit
#include"stdio.h"
void main(void)
{
        printf("Hello? Is that you??!");
}

무척이나 간단해 보이는 소스코드입니다.

하지만, 프로그램의 결과를 정확하게 예측하기에는 보통의 내공을 가지고는 불가능합니다.
한번 도전해 보세요!!!

왜 그런지는 스스로 알아보세요! 정답은 제목에 있습니다.
신고
  1. Favicon of http://rudalson.tistory.com BlogIcon 없다캐라

    | 2014.06.26 14:08 신고 | PERMALINK | EDIT | REPLY |

    잘 모르겠습니다.
    예측한대로 결과가 나오는데요 ^^;;;;

    어떤점을 짚어봐야 되나요? 답이 궁금하네요

Write your message and submit