지금까지 일한곳들에 존재하는, 거지 같은 소스들을 유지, 보수하면서 느낀점들을 정리한다.
sizeof 와 구조체 패딩
결론만 먼저 얘기하면, 구조체 크기를 임의로 가정해서 이 수치를 가지고 연산하지 말라는 것이다. 구조체를 다룰때 네트워크 연동등이 아니라면, 일반적으로 패딩 바이트에 대해 신경 쓸일은 없다. 그런데 네트워크 연동이 아니더라도 구조체 크기를 임의로 판단, offset 으로 멤버에 접근을 시도하면 문제가 될수 있다는 것을 아래에서 보여주고 있다.
먼저, 다음과 같은 형태의 데이터를 빈번하게 파일에 쓰는 경우를 생각해보자.
#define MAX_DATA_SIZE 2048 typedef struct __FileBlock { char strData1 [2]; int nNumber1 ; //4byte int nNumber2 ; //4byte int nNumber3 ; //4byte char strData2 [50]; char strData3 [MAX_DATA_SIZE]; } ItemFileBlock;
그리고 이 구조체 변수에 값을 채우고 파일에 그대로 저장하는 경우를 위한 함수를 하나 작성했다,
bool SaveDataToFile(ItemFileBlock* pstFileBlock) { //m_clsFile 는 파일 I/O 를 담당하는 객체 //구조체를 sizeof(ItemFileBlock) 크기만큼 파일에 저장. if( !m_clsFile.WriteRecord((char*)pstFileBlock, sizeof(ItemFileBlock))) { return false; } return true; }
그리고 이함수를 이용해서 데이터를 저장.
ItemFileBlock stFileBlock; //이제 stFileBlock 의 항목을 채운다. //..... //pData3 는 이미 사용자의 정보를 가르키고 있는 포인터 이다. memcpy(&stFileBlock.strData3, pData3, MAX_DATA_SIZE); //흠 복사 할 게 좀 많네 //함수 호출 if(!SaveDataToFile(&stFileBlock)){ .... }
여기까지는 좋다. 뭐 그냥 파일에 구조체 크기만큼 저장하는거니까 이걸로 OK.
그런데 이 개발자는 strData3 항목에 계속 데이터를 복사하는게 마음에 걸린 모양이다. 이미 이 항목에 저장할 데이터의 포인터(pData3)를 가지고 있어서, 굳이 복사를 안해도 될것 같아 성능 향상(실제로 성능이 향상될거 같지는 않다. file I/O가 추가됬으니 - -)을 목적으로 SaveDataToFile 함수를 다음처럼 수정했다.
//이젠 strData3에 저장될 데이터를 포인터로 받는다. bool SaveDataToFile(ItemFileBlock* pstFileBlock, char* pData3) { //2번에 나눠 파일에 저장. 한번은 strData3 이전 데이터 // !!! sizeof(ItemFileBlock)-MAX_DATA_SIZE 이것은 ?! if( !m_clsFile.WriteRecord((char*)pstFileBlock, sizeof(ItemFileBlock)-MAX_DATA_SIZE) ) { return false; } //strrData3은 여기서 저장된다 if( !m_clsFile.WriteRecord((char*)pData3, MAX_DATA_SIZE)) { return false; } return true; } //그리고 실제 호출시에는 다음처럼 strData3에 불필요한(?) 복사를 생략하여,
//값을 채우지 않는다. ItemFileBlock stFileBlock; //stFileBlock 의 항목을 채운다. ..... ..... //memcpy(&stFileBlock.strData3, 사용자의정보, MAX_DATA_SIZE); //생략! //함수 호출 if(!SaveDataToFile(&stFileBlock, pData3)){ .... }
구조체 크기에서 MAX_DATA_SIZE 만큼 빼면 strData3 항목의 시작위치가 될것이라는 가정..
이렇게 구조체의 선언만 보고, 임의로 구조체 크기를 가정하고 거기에 offset연산까지 적용해서 어떤 구조체 멤버에 접근하려는 시도가 문제되는 부분이다. 원래 작성자의 의도는 구조체의 strData3 이전 데이터를 먼저 파일로 저장후, 이어서 실제 사용자 데이터 부분을 저장하려던 것이었다. 불필요한 복사를 생략하고, 이미 구해진 데이터의 포인터를 활용해보자는 목적이었는데, 문제는 sizeof(ItemFileBlock) 이 돌려주는 길이는 컴파일러에 의해 암묵적으로 삽입된 패딩 바이트를 포함하고 있다는 것이다.
offsetof 매크로를 이용, 각 구조체 멤버의 offset 위치를 구해보자.
#define MAX_DATA_SIZE 2048 typedef struct __FileBlock { char strData1 [2]; int nNumber1 ; //4byte int nNumber2 ; //4byte int nNumber3 ; //4byte char strData2 [50]; char strData3 [MAX_DATA_SIZE]; } ItemFileBlock; void testFunc() { printf("sizeof(ItemFileBlock)[%d]\n",sizeof(ItemFileBlock)); printf( "offsetof(strData1) =%d\n",offsetof(struct __FileBlock, strData1 )); printf( "offsetof(nNumber1) =%d\n",offsetof(struct __FileBlock, nNumber1 )); printf( "offsetof(nNumber2) =%d\n",offsetof(struct __FileBlock, nNumber2 )); printf( "offsetof(nNumber3) =%d\n",offsetof(struct __FileBlock, nNumber3 )); printf( "offsetof(strData2) =%d\n",offsetof(struct __FileBlock, strData2 )); printf( "offsetof(strData3) =%d\n",offsetof(struct __FileBlock, strData3 )); } //실행 결과 sizeof(ItemFileBlock)[2116] offsetof(__FileBlock,strData1) =0 offsetof(__FileBlock,nNumber1) =4 offsetof(__FileBlock,nNumber2) =8 offsetof(__FileBlock,nNumber3) =12 offsetof(__FileBlock,strData2) =16 offsetof(__FileBlock,strData3) =66
즉 다음처럼 패딩 바이트가 삽입됨을 알수있다.
typedef struct __FileBlock { char strData1 [2]; //offset 0 char padding [2]; //offset 2 int nNumber1 ; //offset 4 int nNumber2 ; //offset 8 int nNumber3 ; //offset 12 char strData2 [50];//offset 16 char strData3 [MAX_DATA_SIZE]; //offset 66 char padding [2]; //offset 2114 } ItemFileBlock;
sizeof(ItemFileBlock) 는 모든 구조체 멤버의 실제 길이를 합한 길이 2112 가 아니고,
컴파일러에 의해 삽입된 패딩 바이트를 포함한 2116을 리턴한다.
그래서 sizeof(ItemFileBlock) - MAX_DATA_SIZE 이값은 2116-2048 = 68이 되버린다.
이것이 왜 문제가 되는가?
위 파일 저장 함수에서 2번에 걸쳐 저장을 할때, 처음 쓰기에서는 68 바이트를 저장하고있다. 이렇게 되면 실제 저장되어야할 길이 66 바이트보다 2 바이트를 초과해서 쓴 꼴이 된다. 그리고 2번째 파일 쓰기에서는 2바이트가 밀린 상태로 나머지 strData3을 쓰게 된다. 이런식으로 2번에 걸쳐 파일에 저장시, 실제 파일에 저장되는 구조체의 데이터는, 구조체를 한번에 저장할때와 다른 모습으로 비정상으로 저장된다.
위 파일 저장 함수에서 2번에 걸쳐 저장을 할때, 처음 쓰기에서는 68 바이트를 저장하고있다. 이렇게 되면 실제 저장되어야할 길이 66 바이트보다 2 바이트를 초과해서 쓴 꼴이 된다. 그리고 2번째 파일 쓰기에서는 2바이트가 밀린 상태로 나머지 strData3을 쓰게 된다. 이런식으로 2번에 걸쳐 파일에 저장시, 실제 파일에 저장되는 구조체의 데이터는, 구조체를 한번에 저장할때와 다른 모습으로 비정상으로 저장된다.
* 정상적으로 구조체가 저장된 경우
|---------strData1~2----|---------------strData3--------------|---padding----|
66 bytes 2048 bytes 2 bytes
* 비정상적으로 구조체가 저장된 경우
|---------strData1~2--------------------|---------------strData3-------------|
68 bytes 2048 bytes
68 bytes 2048 bytes
이처럼 잘못 저장된 데이터를 다시 파일에서 읽어들이는 경우는 어떨까?
파일에서 읽어 이를 구조체로 변경하는 경우, strData3 데이터 시작부분이 2 byte가 밀려 있으리란것을 예상할수 있다 (실제로 당하니 멘붕. 데이터를 분명 썼는데 읽으면 안보임 - -). 이것을 해결하기 위해서는 구조체 pack 혹은 명시적으로 패딩 바이트를 삽입하는 방법등이 있는데 그것은 생략한다. 구조체는 일반적인 방법(멤버 참조)으로 쓰는 경우는 문제 없다. 하지만 위에서처럼 offset 으로 멤버의 위치를 유추하려는 시도를 할때는 반드시 암묵적 패딩 바이트를 고려해야만 한다.
댓글 없음:
댓글 쓰기