제목처럼 char 포인터에 관한 내용인지는 잘 모르겠지만
char data[0];
위와 같은 코드가 뜻하는 것이 무엇인지 궁금합니다.
제가 보기에는 char *data와 비슷한 뜻일 것 같은데요.
비슷한 의미라면 왜 저렇게 썻는지, 다른 의미라면 어떤 의미인지 조언부탁드립니다.
구조체의 마지막에 있는 것이 맞습니다. ^^
다른 궁금한 점이 생겼는데요. sizeof(struct header)에 char data[0]가 평가되지 않는다고 하셨는데요. 그럼 어떤 값(여기서는 주소값)을 저장할 공간이 없다는 것인데 어떤 방법으로 포인터로써 사용될 수 있는지 궁금합니다. gcc에서 해결해 주는 꽁수(?)인가요...
구조체의 마지막에 있는 것이 맞습니다. ^^
다른 궁금한 점이 생겼는데요. sizeof(struct header)에 char data[0]가 평가되지 않는다고 하셨는데요. 그럼 어떤 값(여기서는 주소값)을 저장할 공간이 없다는 것인데 어떤 방법으로 포인터로써 사용될 수 있는지 궁금합니다. gcc에서 해결해 주는 꽁수(?)인가요...
저도 초보라서 ^^ 자세한 구현내용은 모르나
다만 header의 바로 다음을 가르키기만 할듯 하니까 매우 쉬울듯 합니다. (항상 constant한 것처럼 보이자나요 header가 있을때 몇바이트 뒤니까요 ^^)
head.data = ~~ ; ==>
(char * ) head + sizeof( struct header) = ~~ 정도로 바뀌지 않을까요?
이런식으로 쓰입니다. 실제 data는 sizeof(struct header)에서 평가되지
않고 저렇게 가상의?? 멤버변수로서 데이터를 포인트 할 수 있게 만듭니다.
제가 알기론 C표준이 아니라 gcc익스텐션으로 알고 있고
char data[0]; 와 같은 statement는 문법위반인지 undefined behavior인지
확실치 모르겠네요 ^^
PS. 잘못된 부분이 있어 수정 합니다. ^^
제가 알고있기로도 위에처럼의 용도로 많이들 사용합니다.
위의 테크닉은 단지 코드의 깔끔함때문에 쓰이는거 같진 않습니다.
대신 데이터의 연속성을 보장해주죠..
예를들면..
struct _msg_header {
int sender;
int owner;
char *msg;
}
이와 같이 할수도 있고
struct _msg_header {
int sender;
int owner;
char msg[1];
}
와 같이 하는경우
전자에서의 header부분을 alloc하고 msg부분을 또 alloc해서 사용하지만 후자는 한번만 한다고 해서 좋은건 아니구요.
메모리의 안정성을 보면, 전자의 케이스인경우 header의 메모리가 어디에 저장되는지.. msg의 데이터가 어디에서 받아오는지 도통 알수가 없는경우가 생깁니다. 유저쪽에선 별문제될 소진 없으나 커널쪽에선 상당히 크리티컬한 버그로 번질수도 있는문제이겠죠.
그래서 이런이유로 후자의 경우와 같은 테크닉을 사용합니다.
"char msg[0]"이 gcc extension인건 오늘알았지만 이 extension이 제공되지 않는곳에서도 사용할려면 위와같이 "char msg[1]"로 두고 alloc할때 1바이트 차감하는식으로 사용합니다.
C FAQs Q 2.6의 세 번째 방법과 비슷하군요.
거기에 나온 제한 조건만 지킨다면 괜찮아 보입니다.
아래의 코드에서는 어떤 문제가 발생할 수 있을까요?
typedef struct S__ {
int i;
T *buf;
} S;
int func(void)
{
S *ps = malloc(sizeof(S) + 1024);
ps->buf = (T*)(ps+1);
}
저는 T가 어떤 형이 되더라도, 아래의 3가지 이유로 문제가 없다고 생각합니다만... 틀린 부분 있으면 지적 바랍니다.
1. 일단, (char *)(ps+1)은 정렬제한을 만족합니다. 그렇지 않다면 S arr[3]의 두번째 요소는 정렬제한을 어기게 되겠지요.
2. 유효한 메모리 공간 바로 그 다음 공간을 가리키는 것은 유효합니다.
3. 모든 배열은 그 원소 사이사이에 padding을 둘 수 없습니다. 따라서 정렬제한을 어겨도 상관없는(즉 char형이나 char형만으로 이루어진 구조체같은) 타입이 아니라면 반드시 n(메모리의 정렬제한을 어기지 않는 숫자)의 배수 크기를 갖게 되어 있습니다. 만약 시작 주소가 유효한 주소라면, (시작주소+n*해당개체의 크기)를 해도 유효한 주소가 나와야 됩니다.
Modern C++ Design에서 C/C++에서 동작하는 코드로 다음을 제시합니다. #define STATIC_CHECK(expr) { char unnamed[(expr)?1:0];}
길이가 0인 배열은 허용하지 않기 때문에(!) 위의 코드는 컴파일 타임에 assertion 처럼 동작한다고 되어 있는데, 위처럼 사용하는게 맞다면 제대로 동작 안하겠네요.
Moder C++ Design에서 C/C++에서 동작하는 코드로 다음을 제시합니다. #define STATIC_CHECK(expr) { char unnamed[(expr)?1:0];}
길이가 0인 배열은 허용하지 않기 때문에(!) 위의 코드는 컴파일 타임에 assertion 처럼 동작한다고 되어 있는데, 위처럼 사용하는게 맞다면 제대로 동작 안하겠네요.
저도 무심코 넘겼는데 그런 지적도 나올 수 있겠군요. 저도 100% 확신할 수 없지만
일단 제 생각을 말씀드리겠습니다.
예를 들어 구조체 자체와 그 멤버인 int와 T*의 정렬 제한이 4의 배수이고 T형의
정렬 제한은 8의 배수인 경우에 정렬 제한 위반이 발생할 수 있음을 쉽게 알 수
있습니다. 이 구조체에 패딩이 없는 구현체라면 (ps + 1)은 4의 배수이지만
반드시 8의 배수라고는 할 수 없기 때문입니다.
이제 문제는 2배수 또는 4배수 등의 정렬 제한을 갖는 T형에 대해서도 정렬 위반이
발생할 수 있느냐는 것인데, 이건 저도 잘 모르겠습니다. 제가 접하는 환경에 길들여져서
사고의 틀이 경직된 것 같습니다. --;
이럴 때 전웅님이 출동하면 좋을 텐데...
구조체의 멤버가 아닌 배열 선언으로는 허용하지 않습니다. 원소의 개수는 반드시
0보다 커야 합니다.
지금 논의되고 있는 것은 배열의 선언이 아니라 이것이 구조체의 멤버로 있을 때
어떻게 되느냐 하는 것입니다. 위에서 말씀드린 gcc의 확장과 C FAQ의 링크를
참고하십시오.
구조체의 멤버가 아닌 배열 선언으로는 허용하지 않습니다. 원소의 개수는 반드시
0보다 커야 합니다.
지금 논의되고 있는 것은 배열의 선언이 아니라 이것이 구조체의 멤버로 있을 때
어떻게 되느냐 하는 것입니다. 위에서 말씀드린 gcc의 확장과 C FAQ의 링크를
참고하십시오.
typedef struct S__ {
int i;
T *buf;
} S;
int func(void)
{
S *ps = malloc(sizeof(S) + 1024);
ps->buf = (T*)(ps+1);
}
기본적으로 어떤 데이터형 X 의 정렬 제한을 align(X) 라고 놓겠습니다. 이
수는 1 이상의 정수가 될 수 있습니다. 물론, sizeof(X) % align(X) == 0
이어야만 합니다 - 그래야만 padding 없이 배열 요소가 모두 올바르게
정렬될 수 있습니다.
이제 malloc() 로 할당된 메모리의 정렬 제한에 대한 이야기부터
시작하겠습니다.
malloc() 가 반환하는 메모리에 가해지는 정렬 제한과 관련된 유일한
제약은, 그 *시작* 주소가 해당 구현체에서 가능한 모든 데이터형에 대해
정렬 제한을 만족해야 한다는 것입니다. 따라서,
int *p = malloc(sizeof(int)*n); // successful allocation assumed
에서 p 는 항상 올바르게 정렬되며, (p+i) 도 i <= n 인 이상 정렬 제한
문제를 겪지 않습니다. 하지만,
p = (int *)((char *)p + m);
에서는 m 이 align(int) 의 배수 (따라서 m % align(int) == 0) 가 아닌
이상 정렬 제한을 위반할 수 있다는 사실에 유의하셔야 합니다.
이제 언급한 문제로 돌아가 보겠습니다. "다른 데이터형을 모두 무시하고
예에 나온 데이터형만을 고려했을 때" malloc() 가 반환하는 메모리는
최소한 LCM(align(S), align(T), int) 에 정렬되는 메모리입니다. [*] 이제
위 코드가 정렬 제한 문제를 일으키는 경우는 sizeof(S) 가 align(T) 의
배수가 아닌 경우를 찾는 문제가 됩니다 (앞서 제가 보인 예와 비교해
보시기 바랍니다). 물론, 이와 같은 경우는 얼마든지 상상할 수 있지만,
여기서는 S 가 구조체라는데 특별히 주목할 필요가 있습니다. 구조체와
관련해서는 다음과 같은 사항이 덧붙습니다.
- 모든 구조체는 공통된 정렬 제한을 갖습니다. [**]
- 모든 (object) 데이터형이 구조체의 첫 번째 멤버로 선언될 수 있습니다.
- 구조체의 가장 앞 부분에는 padding 이 붙지 않습니다.
따라서 구조체는 사실상 malloc() 가 반환하는 메모리의 정렬 제한을
가져야 하고, 결국, (ps+1) 역시 동일한 정렬 제한을 만족하게 되므로
(T *) 데이터형으로의 변환에 아무 문제가 없게 됩니다.
여기까지가 실제 implementation 에서 일어나는 이야기입니다. 이제 표준에
관심있는 분들을 위해 엄격한 표준의 관점으로 들어가 보도록 하겠습니다.
동일한 데이터형이라고 해도 구조체 멤버일 경우와 그렇지 않은 경우 정렬
제한이 달라질 수 있습니다. 즉, 포인터를 위한 "일반화된 정렬 제한"이
존재하고 구조체 안팎에서 서로 다른 정도로 정렬 제한을 강요하는 것이
가능합니다.
예를 들어, 어떤 데이터형 Y 가 12 바이트 크기를 갖고, 데이터형 Y의
대상체가 구조체 멤버인 경우에는 align(Y) = 4, 구조체 멤버가 아닌
경우에는 align(Y) = 6 이라고 가정하겠습니다. 이 경우 12 % 4 == 0,
12 % 6 == 0 이므로 배열과 관련된 문제는 없습니다. 이때 포인터는 구조체
안팎의 대상체를 모두 가리킬 수 있어야 하므로 "일반화된" 정렬 제한은
GCD(4, 6) = 2 가 됩니다. 하지만, 데이터형 Y의 구조체 안팎의 정렬
제한을 생각하면 모든 짝수가 올바른 (Y *) 타입의 포인터 값은 아니라는
것을 알 수 있습니다. 즉, 설명을 위해 주소 공간이 linear 한 경우를
생각해보면 2나 10의 경우 분명 짝수이지만, 해당 번지에는 결코 데이터형
Y의 대상체가 올 수 없음을 쉽게 알 수 있습니다. 이제 이와 같은 경우
(즉, sizeof(S) 가 "일반화된" 정렬 제한의 배수가 아니거나 유효하지 않은
배수가 되는 경우)가 문제로 주어진 경우에도 발생할 수 있는지 생각해
보겠습니다.
구조체 멤버일 때의 정렬 제한을 Salign(), 일반화된 정렬 제한을 Galign()
이라고 부르겠습니다. 지금까지의 내용을 바탕으로 요구되는 관계를
정리하면 다음과 같습니다.
(1), (2), (3) 은 당연한 이야기입니다. (5), (6) 은 "일반화된" 정렬
제한을 구하는 과정에 의한 것이며, (4) 는 S 가 구조체이고, 이 구조체의
정렬 제한은 "최소한" 데이터형 T의 멤버를 첫 번째 멤버로 갖는 다른
구조체와도 같아야 하기 때문에 주어진 조건입니다. 배수 관계는
transitive 하기 때문에 이 조건을 정리하면 sizeof(S) 는 항상 Galign(T),
Salign(T) 의 배수가 됨을 알 수 있습니다. 하지만, sizeof(S) 가 반드시
align(T)의 배수가 될 필요는 없다는 것도 알 수 있습니다. 단, 이미
언급했듯이 앞서 살펴본 경우와 달리 이 경우에는 sizeof(S) 의 배수가
항상 유효한 Galign(T) 의 배수임만을 확인하면, sizeof(S)와 align(T)의
관계는 중요하지 않습니다. sizeof(S) 가 항상 Salign(T) 의 배수이고,
Salign(T) (의 배수)는 항상 유효한 Galign(T) 의 배수이기 때문에 결국
sizeof(S) 가 항상 Galign(T) 의 유효한 배수임을 확인할 수 있습니다.
따라서 엄격한 표준 해석을 적용해도 문제를 일으키지 않습니다.
[*] malloc() 가 정렬 제한을 보장하는 방법에는 모든 type 에 정렬될 수
있는 주소를 취하는 것 이외에도 malloc() 로 할당되는 메모리의 정렬
제한을 char 수준으로 완화되는 것도 가능합니다. 하지만, 이 경우는
제기된 문제를 압박하는 경우가 되지 않으므로 고려하지 않습니다.
[**] 구조체의 정렬 제한과 관련된 문제는 C99에 새로 추가된 요구입니다.
하지만, opaque type 이 작동하기 위해서는 C90 에서도 사실상
있었어야 하는 요구이므로, 모든 표준이 사실상 이를 요구하고 있다고
보아도 무방합니다.
그럼...
p.s. 절 미쳤다고 생각하진 마시고, 그냥 이런거 하면서 재미느끼는 사람도
있구나 정도로 생각해 주시기 바랍니다. --;
"다른 데이터형을 모두 무시하고
예에 나온 데이터형만을 고려했을 때" malloc() 가 반환하는 메모리는
최소한 LCM(align(S), align(T), int) 에 정렬되는 메모리입니다. [*] 이제
위 코드가 정렬 제한 문제를 일으키는 경우는 sizeof(S) 가 align(T) 의
배수가 아닌 경우를 찾는 문제가 됩니다.
malloc()이 반환하는 메모리는 LCM(align(S), align(T*), int)에 정렬되는 것이
아닌지요? (align(T)가 아니라). 만약 align(T)와 align(T*)가 무관하다면
(ps+1)이 T형의 정렬 제한을 항상 만족한다고 할 수는 없지 않나요? T가 char인
경우에는 항상 만족하겠지만요.
표준을 엄격하게 적용했을 때의 설명도 구조체의 멤버가 T형일 때 적용되는 것이
아닌가 합니다. 멤버가 T* 형일 때는 달라지지 않나요?
"다른 데이터형을 모두 무시하고
예에 나온 데이터형만을 고려했을 때" malloc() 가 반환하는 메모리는
최소한 LCM(align(S), align(T), int) 에 정렬되는 메모리입니다. [*] 이제
위 코드가 정렬 제한 문제를 일으키는 경우는 sizeof(S) 가 align(T) 의
배수가 아닌 경우를 찾는 문제가 됩니다.
그러고 보니 잘못 적었군요. 의도한 바는 LCM(align(S), align(T),
align(int), align(T *)) 정도가 되겠네요. 보다 정확히 적자면,
LCM(align(S), align(T), align(int), align(T *), C 언어의 정렬 제한이
다른 가능한 모든 타입...) 가 되어야 합니다. 따라서 align(T) 냐
align(T *) 냐는 답과는 상관 없습니다.
doldori wrote:
만약 align(T)와 align(T*)가 무관하다면
(ps+1)이 T형의 정렬 제한을 항상 만족한다고 할 수는 없지 않나요? T가 char인
경우에는 항상 만족하겠지만요.
아닙니다. 일단 ps 가 모든 type 에 대해서 정렬이 되어야 하므로, (ps+1)
역시 모든 type 에 대해서 정렬이 되어야 합니다.
sizeof(S) % align(S) = 0 이라는 점을 적용해 보시기 바랍니다.
doldori wrote:
표준을 엄격하게 적용했을 때의 설명도 구조체의 멤버가 T형일 때 적용되는 것이
아닌가 합니다. 멤버가 T* 형일 때는 달라지지 않나요?
적혀 있듯이, 구조체의 첫 번째 멤버로 T 형을 갖는 구조체와도 문제 없는
정렬 제한을 가져야 합니다. 물론, 첫 번째 멤버로 T * 형을 갖는
구조체와도 문제가 없어야 하구요. 문제를 해결할 때 중요한 것은 첫번째
멤버로 T 형을 갖는 구조체이기에 그것만 고려했을 뿐입니다. T * 형을
계산에 추가하는 것이 답에 영향을 주지는 않으며, T 형을 빼는 것은
불가능합니다.
네, 보충설명 감사합니다. 제가 구조체와 정렬에 대해서 많은 부분을 잘못 알고
있었군요. 그럼 C FAQ 2.6의 내용은 고쳐야겠는데요.
Quote:
그러나, 위와 같이, malloc을 한 번 불러서, 두번째 영역까지 할당하는 것은, 두 번째 영역이 char 배열로 취급될 경우에만 이식성이 있습니다. 다른, 더 큰 데이터 타입을 쓴다면, alignment (질문 2.12, 16.7 참고) 문제가 발생할 가능성이 높습니다.
이것을 Steve Summit씨에게 알려준다고 해도 그냥 고치지는 않을 것이고
먼저 clc에서 논의가 돼야 할 텐데, 영어로 이런 긴 얘기를 꺼낸다는 것부터가
어렵겠는데요. -_-; cinsk님은 어떻게 생각하시나요?
네, 보충설명 감사합니다. 제가 구조체와 정렬에 대해서 많은 부분을 잘못 알고
있었군요. 그럼 C FAQ 2.6의 내용은 고쳐야겠는데요.
Quote:
그러나, 위와 같이, malloc을 한 번 불러서, 두번째 영역까지 할당하는 것은, 두 번째 영역이 char 배열로 취급될 경우에만 이식성이 있습니다. 다른, 더 큰 데이터 타입을 쓴다면, alignment (질문 2.12, 16.7 참고) 문제가 발생할 가능성이 높습니다.
이것을 Steve Summit씨에게 알려준다고 해도 그냥 고치지는 않을 것이고
먼저 clc에서 논의가 돼야 할 텐데, 영어로 이런 긴 얘기를 꺼낸다는 것부터가
어렵겠는데요. -_-; cinsk님은 어떻게 생각하시나요?
사실 위에서 언급했듯이 모든 구조체가 같은 정렬 제한을 가져야 한다는
요구가 없으면 (증명 과정에서 가장 중요한 근거가 사라지게 되므로)
문제의 코드는 당연히 정렬 제한 문제를 겪으며, C FAQ 2.6 역시 고칠
필요가 없습니다.
하지만, 그와 같은 (사실상 C90 에도 내재되어 있던) 요구가 C99 에
명시적으로 추가되었기 때문에 가시적인 차이가 발생하게 되었습니다.
사실상 struct hack 에 대한 해결책을 제시하기 위해 추가된 부분이 아닌데
본의 아니게 그와 같은 역할을 할 수 있게 된 것 같습니다.
매우 pedantic 한 입장에서 제가 마지막으로 의심을 갖는 부분은 "모든"
구조체가 동일한 정렬 제한을 가져야 한다는 요구 조건이 "해당 구현체에서
가능한 모든 구조체"에 적용되는 것인지, 아니면 "어떤 프로그램에
존재하는 모든 구조체"에 적용되는 것인지가 불분명합니다.
문제가 되는 부분이 일반적인 type 을 정의하고 설명하는 부분이기 때문에,
또한 그와 같은 요구가 도입된 가장 큰 배경이 opaque type 지원이기
때문에, 이론적으로나 현실적으로나 "해당 구현체에서 존재하는 모든
구조체"에 적용되는 것으로 보는 것이 타당한 것 같습니다만, 제가
유권해석을 내릴 수 있는 입장이 아니라 장담은 못합니다. 이 부분은 곧
csc 에서 한번 논의해보려 합니다 - 만약, 해당 논의에서 제 주장과 동일한
결론이 나오는 경우 Steve Summit 씨께는 해당 논의 링크를 전달하는 것
만으로도 충분할 것입니다.
하지만, C FAQs 의 성격을 보았을 때 수정이 이루어지지 않을 가능성이
크다고 봅니다. 질문 2.6 이외에도 이미 다른 몇몇 부분에서 설명을 쉽게
하기 위해 잘못된 설명을 한 부분이 있지만, 실용적인 프로그래밍에서는
크게 영향을 미치지 않는 부분들입니다. 이 부분 역시 이미 C99 에서 FAM
를 지원하는 상황에서 그와 같은 설명을 굳이 수정, 추가할 이유는 그리
커보이지 않습니다.
흠... 이 문제로 처음 생각했던 것보다 한참을 고민하게 되었습니다.
덕분에 제가 잘못 알고 있던 부분도 확실히 고쳐 알 수 있는 계기가
되었습니다.
저 위에 틀린 내용들이 많이 있지만, 겉으로는 번지르해 보이는 논리가
틀릴 수도 있다는 사실을 알리고자(--;) 굳이 편집하거나 지우지 않고
아래에 새로 답을 달도록 하겠습니다.
우선, 중요한 내용 하나를 정정하자면,
C90에 이미 암시되어 있고, C99에 명시적으로 추가된 구조체와 관련된 정렬
제한 내용은, 구조체 자체가 아닌 구조체 포인터에 대한 정렬 제한 이야기
입니다. 오랜만에 표준 인쇄본을 펼치니 해당 페이지에 "구조체가 아닌
포인터에 대한 이야기임!"이라고 메모가 되어 있는데도 정성을 덜 들인
탓인지 금새 잊어버리고 말았네요. 따라서 모든 구조체가 반드시 같은 정렬
제한을 가질 필요는 없습니다. 이런 사실을 바탕으로 앞서 말씀드렸던
잘못된 답변을 올바르게 고쳐 정리하겠습니다.
Anonymous wrote:
아래의 코드에서는 어떤 문제가 발생할 수 있을까요?
typedef struct S__ {
int i;
T *buf;
} S;
int func(void)
{
S *ps = malloc(sizeof(S) + 1024);
ps->buf = (T*)(ps+1);
}
기본적으로 어떤 데이터형 X 의 정렬 제한을 align(X) 라고 놓겠습니다. 이
수는 1 이상의 정수가 될 수 있습니다. 물론, sizeof(X) % align(X) == 0
이어야만 합니다 - 그래야만 padding 없이 배열 요소가 모두 올바르게
정렬될 수 있습니다.
이제 malloc() 로 할당된 메모리의 정렬 제한에 대한 이야기부터
시작하겠습니다.
malloc() 가 반환하는 메모리에 가해지는 정렬 제한과 관련된 유일한
제약은, 그 *시작* 주소가 해당 구현체에서 가능한 모든 데이터형에 대해
정렬 제한을 만족해야 한다는 것입니다. 따라서,
int *p = malloc(sizeof(int)*n); // successful allocation assumed
에서 p 는 항상 올바르게 정렬되며, (p+i) 도 i <= n 인 이상 정렬 제한
문제를 겪지 않습니다. 하지만,
p = (int *)((char *)p + m);
에서는 m 이 align(int) 의 배수 (따라서 m % align(int) == 0) 가 아닌
이상 정렬 제한을 위반할 수 있다는 사실에 유의하셔야 합니다.
이제 언급한 문제로 돌아가 보겠습니다. "다른 데이터형을 모두 무시하고
예에 나온 데이터형만을 고려했을 때" malloc() 가 반환하는 메모리는
최소한 LCM(align(S), align(T), align(int), align(T *), ...) 에
정렬되는 메모리입니다. [*] 이제 위 코드가 정렬 제한 문제를 일으키는
경우는 sizeof(S) 가 align(T) 의 배수가 아닌 경우를 찾는 문제가 됩니다
(앞서 제가 보인 예와 비교해 보시기 바랍니다). 이와 같은 경우는 충분히
상상해 볼 수 있기 때문에 해당 코드가 정렬 제한 문제를 일으킬 수 있다는
사실은 쉽게(비록 전 쉽게 알지 못했습니다만) 알 수 있습니다.
이제 조금 더 복잡한 경우로 구조체 S 가 다음과 같은 선언된 경우를
생각해 보겠습니다. (솔직히 저 위에서 멈추기에는 민망해서 예 하나 더
추가합니다 ;-)
typedef struct S__ {
T dummy;
int i;
T *buf;
} S;
S object, *foo = &object;
첫 번째 멤버로 T dummy; 를 넣은 이유는 구조체 S 의 padding 을 결정할
때 이 구조체 타입의 배열이 고려되어야 하기 때문에 foo 는 물론, foo+1
역시 T 형에 올바르게 정렬된다는 사실을 얻기 위해서 입니다.
물론, 이와 같은 생각 아래에는 다음과 같은 가정이 깔려 있습니다.
- p 가 데이터형 T 에 대해 올바르게 정렬되어 있고 p 가 가리키는 곳에
데이터형 T 의 대상체가 존재하는 경우, p+1 역시 항상 데이터형 T 에
대해 올바르게 정렬된다.
실재하는 구현체를 고려했을 때 이 가정이 맞지 않는 구현체는 없다고 확신
합니다. 따라서 처음 주어진 코드에 T dummy; 를 첫번째 멤버로 추가해
주는 경우 실제 구현체에서는 정렬 제한 문제를 겪지 않습니다.
하지만, 제가 적은 가정이 표준에 의해서 명백하게 보장되지는 않습니다.
표준에 의하면 정렬(alignment)은 대상체(object)에 대해 정의되어
있습니다. 즉, 대상체가 존재하지 않으면 정렬 역시 존재하지 않습니다.
따라서,
int i, *pi = &i;
short *ps = (short *)pi;
이와 같은 코드는 정렬 제한 문제를 겪지만,
int *pi = NULL;
short *ps = (short *)pi;
이와 같은 코드는 정렬 제한 문제를 겪지 않습니다. 널 포인터 자체가
"아무 것도" 가리키지 않는 포인터이기 때문에 대상체가 관여할 수 없고
결국 정렬 제한이 적용될 수 없기 때문입니다.
하지만,
typedef struct S__ {
T dummy;
int i;
T *buf;
} S;
S object, *foo = &object;
이 코드에서 foo+1 의 의미에는 다소 문제가 있습니다. 분명, foo+1 이
어떤 대상체를 가리키는 것은 결코 아니지만, 그렇다고 널 포인터마냥 아무
것도 가리키지 않는다고 할 수도 없는 상황이기 때문입니다. 이 부분에
대해서 표준이 침묵하고 있기 때문에 foo+1 의 정렬에 대한 가정이
만족된다고 확신할 수는 없습니다만, foo+1 의 정렬 제한이 만족됨을
보장해주지 않을 경우 매우 무의미하며 이상한 결론으로 귀결되기 때문에
필히 그 의도는 foo+1 의 정렬을 보장해주는 것이라 믿고 있습니다.
물론, 이와 같은 흐름의 해석에서는 첫번째 멤버로 데이터형 T 의 대상체가
나왔기 때문에 더 이상 구조체 안팎에서 다른 정렬 제한을 갖는 이론적
이야기는 다룰 필요가 없습니다 - 하지만, 몇몇 분들에게는 흥미로울 수
있는 이야기라 생각하기에 삭제하지 않았습니다.
[*] malloc() 가 정렬 제한을 보장하는 방법에는 모든 type 에 정렬될 수
있는 주소를 취하는 것 이외에도 malloc() 로 할당되는 메모리의 정렬
제한을 char 수준으로 완화되는 것도 가능합니다. 하지만, 이 경우는
제기된 문제를 압박하는 경우가 되지 않으므로 고려하지 않습니다.
역시 까다로운 문제를 건드리기 시작하면 어김없이 표준의 "어두운"
부분으로 들어가게 되어있나 봅니다. 처음엔 자신만만하게 답변을 드리려는
의도였는데, 결과적으로 저 역시 많은 것들을 배워 갑니다. 감사합니다.
align(int) % align(short) != 0 일때 문제가 생깁니다. 간단하게 int형이 6바이트의 정렬제한을 가지고 있고 short형이 4바이트의 정렬제한을 가지고 있다면, int형의 주소가 6인 경우 이것을 short *ps에 대입하면 문제가 생길 수 있습니다.
malloc 구현은 하드웨어 의존적입니다. 그러므로 정렬이 필요한
하드웨어 시스템이라도 항상 malloc 이 리턴하는 주소가 C 에서
지원하는 모든 타입의 최소 공배수가 되지는 않습니다.
예를들어 대개의 하드웨어는 FPU 에서 double 계산을 지원하지만
long double 계산은 지원하지 않습니다. 이런 경우 long double
은 소프트웨어 루틴에 의해 계산되고 정렬 제한이
sizeof(long double)이 아닐 수가 있습니다. 이런 시스템인 경우
long double 은 최소 공배수 계산에서 배제될 수 있지요.
아무튼 지원되는 모든 타입의 최소 공배수를 옵셋으로 하면
모든 정렬 문제는 피할 수 있습니다. 최적으로 피하는 건 아니지만요.
그리고 위에서 언급된 코드
int func(void)
{
S *ps = malloc(sizeof(S) + 1024);
ps->buf = (T*)(ps+1);
}
align(int) % align(short) != 0 일때 문제가 생깁니다. 간단하게 int형이 6바이트의 정렬제한을 가지고 있고 short형이 4바이트의 정렬제한을 가지고 있다면, int형의 주소가 6인 경우 이것을 short *ps에 대입하면 문제가 생길 수 있습니다.
맞는 말 같습니다.
하지만, 예제에 있는 내용은 short형이 아닌 pointer 형이란 점에서
차이가 있지 않을까하는 생각입니다.
malloc 구현은 하드웨어 의존적입니다. 그러므로 정렬이 필요한
하드웨어 시스템이라도 항상 malloc 이 리턴하는 주소가 C 에서
지원하는 모든 타입의 최소 공배수가 되지는 않습니다.
물론입니다. 하지만, 위에서 OP 께서 보여주신 코드를 다룰 때 "설명이
용이하면서도 가장 압박할 수 있는" 환경은 바로 malloc() 가 반환한
메모리의 정렬 제한이 모든 type 의 정렬 제한의 최소 공배수가 되는 경우
입니다. "[*]" 부분에서 말씀드렸듯이 가장 완화되는 경우는 malloc() 가
반환하는 메모리 영역에 아예 정렬 제한이 요구되지 않는 경우입니다.
체스맨님께서 아래 언급해주신 상황은 그 두 극단 사이에 있는 상황입니다.
두 극단이든 그 중간의 한 상황이든, 어떠한 경우에도 malloc() 가
반환하는 메모리는 모든 type 에 대해 정렬 제한 문제를 겪지 않아야
합니다. 또한, malloc() 가 반환한 메모리가 특정 type 의 배열로 사용될
경우를 고려해 (type *)"malloc'ed storage" + n 역시 정렬 제한 문제를
겪지 않아야 합니다. 이와 같은 요구를 반영해 정렬 제한 문제를
설명하고자 할 때 alignment modulus 를 사용한다면 LCM(...) 가 가장
적절합니다 (그렇게 하지 않으면 일반화된 해석을 얻기 어렵습니다).
체스맨 wrote:
예를들어 대개의 하드웨어는 FPU 에서 double 계산을 지원하지만
long double 계산은 지원하지 않습니다.
C 언어의 입장에서는 double 과 long double 이 type algebra 를 제외하면
동일하게 다루어지는 type 일 수 있기 때문에 여기서는 "long double" 로
IEEE 754 의 extended (double) precision 을 의미하시는 것으로
이해하겠습니다.
"대개의 하드웨어"에 x86 계열이 포함되지 않는다면 "대개의 하드웨어"가
long double 계산을 지원하지 않습니다. x86 및 그 클론은 extended-based
system 의 대표적인 예입니다. 즉, IEEE 754 의 single precision/double
precision 을 full support 하지 않으며, double-extended precision 을
full support 하고 있습니다.
체스맨 wrote:
아무튼 지원되는 모든 타입의 최소 공배수를 옵셋으로 하면
모든 정렬 문제는 피할 수 있습니다. 최적으로 피하는 건 아니지만요.
여기서 "최적이 아니다"란 말씀은 "불필요하게 엄격한 부분이 있다" 는
의미로 이해하겠습니다. 언급했듯이 malloc() 가 반환하는 메모리에
가해지는 요구가 alignment modulus 로 기술되어 있지는 않습니다. 따라서
각 구현체마다 다양한 방법으로 표준 요구를 만족할 수 있습니다. 예를 들어,
다음과 같은 경우도 가능합니다.
char 를 제외한 모든 type 의 "일반화된" 정렬 제한은 2 입니다. 단,
sizeof(type) >= 4 인 모든 type 은 지역 선언에 의해 선언된 type 일 경우
에는 성능상의 이유로 sizeof(type) 을 정렬 제한으로 갖습니다. 따라서
align(double) == 8 이며, align(long double) == 10, align(int) == 4
등이 됩니다. 하지만, 말씀하신 "최적"에 가깝게 가기 위해 malloc() 를
통해 할당된 메모리 영역의 정렬 제한이 2 가 될 수 있습니다. 이 경우
align(malloc'ed storage) % 4 != 0 && align(malloc'ed storage) % 8 != 0
이어도 "일반화된" 정렬 제한이 2 이기에 실제적인 정렬 제한 문제를 겪지
않습니다.
혹은 좀 더 그럴싸한 예로, sizeof(type) >= 4 인 모든 type 의 정렬
제한은 4 입니다. 따라서 sizeof(type) % 4 == 0 인 type 은 큰 문제를
겪지 않지만, sizeof(type) % 4 != 0 인 type 은 문제가 될 수 있습니다.
따라서 long double 에 맵핑되는 extended precision 은 IEEE 754 를 따라
실제 10 바이트이지만, 이와 같은 정렬 제한 문제 때문에 long double 에
2 바이트의 패딩을 담습니다 - 특정 환경이 번뜩 떠오르는 분들도 계실
겁니다.
정렬 제한과 관련된 많은 부분은 implementation-defined 입니다. 저는
줄곧 흔히 "alignment modulus" 라고 부르는 방법으로 이를 설명하고
있지만 위원회 멤버 중에는 이런 방식의 설명을 못마땅하게 생각하는
사람도 있습니다 - 하지만, 최소한 "alignment" 의 정의가 이를 부분적으로
지지하고 있다고 생각합니다. 하지만, 이미 "일반화된 정렬 제한"의 예에서
볼 수 있듯이 "align(type) == 단일 정수" 의 단순한 모델은 적용할 수
없음은 분명합니다.
체스맨 wrote:
그리고 위에서 언급된 코드
int func(void)
{
S *ps = malloc(sizeof(S) + 1024);
ps->buf = (T*)(ps+1);
}
예, 가능한 방법입니다. 하지만, C99 이전에는 가급적 struct hack 을
위해서 그야말로 "hack" 을 쓰지 않기를 추천합니다. 굳이 hack 을 써야
한다면 C90 표준의 지원 여부를 떠나 C99 의 FAM 과 유사한 형태로
사용하시길 추천합니다 - 어차피 C FAQs 에서 C90 표준이 지원하지
않는다는 두 형태가 실제 작동하지 않는 환경은 찾기 어렵습니다. 즉, 추후
C99 환경으로 이전할 때 가능한 수정이 적은 방향으로 작성하는 것이
유리합니다. 이때 다른 부분은 큰 문제를 일으키지 않지만 sizeof(구조체)
에 유의하셔야 합니다. C99 에서 FAM 이 포함된 구조체의 경우 정렬 제한
만족을 위한 padding 만 크기에 고려될 뿐, 실제 FAM 은 포함되지
않습니다.
flexible array member 는 C 에 항상 있습니다. 단지 [0] 이나 []
처럼 선언하는 것을 지원하지 않을 뿐, 배열 크기로 1 이상의
값 (대개는 1)을 주고 malloc 할 때 잘 조절해주면 되니까요.
의도는 이해하겠습니다만 다소 오해의 여지가 있습니다. FAM 은 C99
이전에는 없습니다. FAM 은 이제 C99 에 추가된 새로운 feature 를
지칭하는 용어입니다.
C FAQs 에서 보인 흔히 "struct hack" 이라고 부르는 트릭은 C99 의 FAM 이
도입된 이후부터는 FAM 이라고 부르기 부적절합니다.
애초 "struct hack" 의 대표적인 2가지 방법이 문제가 되는 것은 표준이
해당 코드 모두를 정의되지 않은 것으로 명시했기 때문입니다 - 이것이
C99 에 FAM 이 도입된 계기입니다.
이와 같은 내용은 doldori 님이 인용해 주신 두 링크(C FAQs)에 잘
설명되어 있습니다.
매지 wrote:
(근데 C++표준에는 저게 없나요?)
체스맨 wrote:
그리고 C++ 표준에는 flexible array member 가 결코 들어갈 수
없습니다. new 를 오버로딩하지 않는한 절대 불가능하니까요.
그렇지 않아도 복잡함이 극에 달하는 C++ 표준에 FAM 이 무리 없이
들어가기는 결코 쉽지 않다는 사실에 동의합니다. 하지만, 가능한 C/C++ 의
차이를 줄여 컴파일러를 개발하는 것이 유리한 개발사 입장에서는 확장을
통해 C++ 에서도 C99 의 기술을 지원하고 있습니다 - 물론, 실질적으로
지원을 하는 것과 언어 정의에 문제 없이 포함되는 것에는 엄청난 차이가
있을 수 있습니다.
align(int) % align(short) != 0 일때 문제가 생깁니다. 간단하게 int형이 6바이트의 정렬제한을 가지고 있고 short형이 4바이트의 정렬제한을 가지고 있다면, int형의 주소가 6인 경우 이것을 short *ps에 대입하면 문제가 생길 수 있습니다.
맞는 말 같습니다.
하지만, 예제에 있는 내용은 short형이 아닌 pointer 형이란 점에서
차이가 있지 않을까하는 생각입니다.
정렬 제한과 관련해 약간의 오해가 있는 것 같습니다. 지극히 이상적인
상황이지만 다음과 같은 경우를 생각해 보겠습니다.
1. 메모리의 addressing system 으로 linear address space 를 가정합니다.
즉, 바이트 단위로 0 부터 메모리의 끝까지 연속적인 주소 공간이
형성되고, 포인터의 값은 바로 그 주소값을 나타냅니다. 예를 들어,
포인터가 12번지에 있는 대상체를 가리킨다면 그 포인터의 값은 정수로
변환했을 때 12가 됩니다.
2. 특정 type T 에 대한 정렬 제한을 간단히 "align(T) == 단일 정수"로
표현할 수 있다고 가정합니다.
3. sizeof(short) == 2, sizeof(int) == 4 를 가정합니다.
4. align(short) == 2, align(int) == 4 를 가정합니다.
이때 align(short) == 2 란 이야기는 short 데이터형의 대상체는 모두 2의
배수 번지에 놓여야 한다는 것을 의미하며, 유사하게 align(int) == 4 란
이야기는 int 데이터형의 대상체가 모두 4의 배수 번지에 놓여야 한다는
것을 의미합니다. 따라서 short 데이터형의 포인터의 값은 항상 2의 배수가
되어야 하며, int 데이터형의 포인터는 항상 4의 배수가 되어야 합니다.
포인터 변환시 정렬 제한 문제가 만족되어야 한다는 것은, int * 는 항상
4의 배수 값을 가져야 하며, short * 는 항상 2의 배수 값을 가져야 한다는
것을 의미합니다. 물론, 일부 계열에서는 정렬 제한이 만족되지 않는 경우
성능 저하 정도로 끝나지만, 다른 환경에서는 프로그램 강제 종료까지 만날
수 있습니다.
이제 제가 처음에 든 예를 (이론상 잘못된 것은 아니지만 현실적으로
부적절한 면이 있으므로) 다음과 같이 바꾸겠습니다.
short arr;
int *pi = (int *)&arr;
이때 arr 이 12번지에 할당되었다고 가정하겠습니다. 이 경우 &arr 의 값은
12가 되며, 12 는 2의 배수인 동시에 4의 배수도 될 수 있기 때문에 int *
의 값이 되는데 아무런 문제가 없습니다.
하지만, arr 이 14번지에 할당되는 경우 short * 로는 문제가 없지만 int *
가 가져서는 안 되는 값이 되기 때문에 정렬 제한 문제를 일으킬 수
있습니다.
실제 예를 확인해 보면 다음과 같습니다.
purple% cat > t.c
#include <stdlib.h>
int main(void)
{
short *ps = malloc(sizeof(*ps) * 4);
int *pi = (int *)(ps+1); /* undefined behavior */
*pi = 0;
return 0;
}
purple% cc t.c
purple% ./a.out
Bus Error (core dumped)
여기서 한가지 유의하실 부분은 포인터 변환시 정렬 제한을 어기게 되면
바로 변환이 이루어지는 시점부터 정의되지 않은 행동이 된다는 것입니다.
즉, 실제로는 위의 예처럼 직접 해당 메모리에 접근하려 할 때 문제가 되는
경우가 대부분이지만, 이론상으로는 (int *)(ps+1) 에서 프로그램이 오작동
하는 것도 가능합니다. 직접 본 적은 없지만 실제 정렬 제한을 만족하지
않는 변환일 경우 구현체 차원에서 포인터 값을 조정해 정렬 제한을
만족하도록 해줄 수도 있다고 합니다. 이 경우 (short *)(int *)(ps+1) 가
원래 포인터인 (ps+1) 과 다른 값이 되는 결과가 나올 수도 있습니다.
정렬제한 문제는 서로 다른 자료형들이 위치할때
발생하는 문제로 생각됩니다만(개인적인 생각),
정렬 제한은 메모리 상의 모든 대상체에 적용되는 개념이며, 정렬 제한이
문제를 일으킬 수 있는 대표적인 경우가 포인터 변환과 구조체 패딩입니다.
정렬 제한은 "특정 type"을 갖는 "대상체"가 "특정 위치"에 놓여야 한다는
요구입니다. 따라서 앞앞앞에 말씀드린 예에서처럼 그 엄격함의 차이가
있을 순 있지만, malloc()로 할당된 대상체든 일반적으로 선언된 변수든
기본적으로 char 를 제외한 모든 type 의 대상체에 적용됩니다 - char 는
그 type 의 특성으로 인해 항상 가장 덜 엄격한 정렬 제한을 갖습니다.
lovewar wrote:
나중에 설명하신 예제들은 연속적인 메모리를 확보하고 그중에서
데이타를 참조(케스팅)할때의 예제라서 이해하기 힘듭니다
물론, 다음과 같은 경우도 충분히 가능합니다.
short s, *ps = &s;
int *pi = (int *)ps; /* undefined behavior */
short s[4], *ps = s+1;
int *pi = (int *)ps; /* undefined behavior */
제가 위와 같은 코드를 사용하지 않고 malloc()로 할당한 메모리를 예로
든 이유는 정렬 제한이 아닌 다른 이유로 undefined behavior 가 관여하지
않도록 하기 위해서입니다.
이미 말씀드렸듯이 표준 관점에서는 포인터 변환이 이루어지는 시점 부터
정렬 제한으로 인한 undefined behavior 가 발생하지만, 실제 구현체에서
그 영향(bus error)을 가시적으로 확인하기 위해서는 변환된 포인터 값을
사용해 실제 메모리를 접근(예를 들면, *pi = 0)할 필요가 있습니다.
그런데, 위 두 코드 모두에서 *pi = 0; 을 할 경우 정렬 제한이 아닌 다른
문제(표준에서는 흔히 "object problem" 이라고 부릅니다)로 undefined
behavior 가 관여하게 됩니다. 따라서 그와 같은 가능성을 철저히 배제하고
순수하게 정렬 제한 문제로만 발생하는 undefined behavior 의 영향을
보이기 위해 처음 설명과는 달리 malloc() 로 할당한 메모리를 사용한
것입니다.
이미 말씀드렸듯이 표준 관점에서는 포인터 변환이 이루어지는 시점 부터
정렬 제한으로 인한 undefined behavior 가 발생하지만, 실제 구현체에서
그 영향(bus error)을 가시적으로 확인하기 위해서는 변환된 포인터 값을
사용해 실제 메모리를 접근(예를 들면, *pi = 0)할 필요가 있습니다.
그런데, 위 두 코드 모두에서 *pi = 0; 을 할 경우 정렬 제한이 아닌 다른
문제(표준에서는 흔히 "object problem" 이라고 부릅니다)로 undefined
behavior 가 관여하게 됩니다. 따라서 그와 같은 가능성을 철저히 배제하고
순수하게 정렬 제한 문제로만 발생하는 undefined behavior 의 영향을
보이기 위해 처음 설명과는 달리 malloc() 로 할당한 메모리를 사용한
것입니다.
설명 감사합니다.
이해했습니다.
-- 덧붙이는 글 --
넘 무지해서 예제를 이해하는데 시간이 걸렸습니다.
예제 작성시 특정부분만을 설명하는 예제를 사용해 주시면 감사합니다.
저 같은 초보를 위해서
제가 이해했던 것을 도형으로 그립니다.
short s, *ps = &s;
int *pi = (int *)ps; /* undefined behavior */
다음을 가정합니다.
정렬은 4의 배수로 한다는 가정하여
short의 크기는 2byte
포인터의 크기는 4byte인 경우
short s가 정렬제한에 위치했을때.
short s |_______|
+1 |_______|
|_empty_| // 정렬 문제
|_empty_| // 정렬 문제
short *ps |_______| // short s의 번지가 저정됨
|_______|
|_______|
+3 |_______|
int *ps |_______| // short s의 번지가 저장킴
|_______|
|_______|
+3|_______|
*ps 을 접근하는 것은 정렬 제한으로 인한 문제가 발생할 수 있다.
※ 이 그림은 물리적으로 연속적인 메모리 구조를 갖을때의 그림입니다.
정렬 제한은 보통 해당 환경의 CPU 의 능력과 효율 등과 관련된 경우가
많습니다. 설명을 위해 이번에도 단순한 설명을 위해 가상의 예를
들겠습니다.
"Gibralt 7000" (--;;) 은 32-bit CPU 를 탑재한 기종으로 메모리에서
4 바이트(32 비트) 단위로 읽어오는 명령어만을 지원합니다. 단, 임의의
번지에서 4 바이트가 아니라, 0, 4, 8, ... 4n 번지에서 4 바이트씩 읽어
오도록 구성되어 있습니다. 이 경우 3번지에 놓인 문자형 자료를 읽어올
경우 0번지부터 4바이트를 읽은 후에 적절히 shift 와 bit-masking 을 해
원하는 바이트를 얻게 됩니다.
을 실행하면 (구현체가 변환 결과가 int 형에 적절히 정렬되지 않음을 알고
미리 그 값을 조정하지 않는 이상) 다음과 같은 형태가 된다고 볼 수
있습니다.
+-----------+-----------+-----------+-----------+
| short [0] | [1] | [2] | [3] |
+-----------+-----------+-----------+-----------+
| |
|
pi 가 가리킴
이제
*pi;
을 실행하면 2번지부터 4바이트를 가져와야 하는 상황이 발생합니다. 즉,
메모리 접근 연산 "한번"으로는 4바이트를 로드할 수 없게 되는 것입니다.
따라서 보통 다음과 같은 선택이 내려질 수 있습니다.
1. int 형의 정렬 제한 위반으므로 그와 같은 접근이 거절 된다.
2. 성능의 손해를 감수하고 첫 번째 워드 32비트를 읽어 그 중 두 바이트와
두 번째 워드 32비트를 읽어 그 중 두 바이트만을 취해 int 형의 값
4 바이트를 구성한다.
즉, 이 선택은 구현체에게 맡겨집니다. 표준 C 언어의 입장에서는 이미
변환 시점부터 undefined behavior 를 일으킨 것이기에 이 외의 다른
선택도 가능합니다. 그리고 제가 보여드린 SunOS 에서의 예는 구현체가
첫번째 경우를 선택한 경우에 해당합니다.
lovewar wrote:
다음을 가정합니다.
정렬은 4의 배수로 한다는 가정하여
short의 크기는 2byte
포인터의 크기는 4byte인 경우
short s가 정렬제한에 위치했을때.
short s |_______|
+1 |_______|
|_empty_| // 정렬 문제
|_empty_| // 정렬 문제
short *ps |_______| // short s의 번지가 저정됨
|_______|
|_______|
+3 |_______|
int *ps |_______| // short s의 번지가 저장킴
|_______|
|_______|
+3|_______|
*ps 을 접근하는 것은 정렬 제한으로 인한 문제가 발생할 수 있다.
※ 이 그림은 물리적으로 연속적인 메모리 구조를 갖을때의 그림입니다.
단지 그림에서 정렬 문제라고 표시해주신 부분이 비어있기(empty) 때문에
문제가 되는 것이 아닙니다. 즉, 정렬 제한은 메모리에 저장된 "값"의 문제가
아니라 메모리 "접근" 자체의 문제입니다.
을 실행하면 (구현체가 변환 결과가 int 형에 적절히 정렬되지 않음을 알고
미리 그 값을 조정하지 않는 이상) 다음과 같은 형태가 된다고 볼 수
있습니다.
+-----------+-----------+-----------+-----------+
| short [0] | [1] | [2] | [3] |
+-----------+-----------+-----------+-----------+
| |
|
pi 가 가리킴
궁금한것이 있어 질문드립니다.
(int *)(a+1) // a[1]
>>구현체가 변환 결과가 int 형에 적절히 정렬되지 않음을 알고
미리 그 값을 조정하지 않는 이상
아니면
(int *)(a+1) // a[2],
//구현체가 변환 결과가 int 형에 적절히 정렬되지 않음을 알고
미리 그 값을 조정한 경우
이렇게(위 코드의 주석처럼) 이해해도 되는 건가요?
아니면, 형변환으로 int 자료형 만큼 이동해야 한다고 생각해야 하나요?
또한번 잘못 이해 했습니다. 의 궁금증은 사라졌으며 바로 위 내용을
읽었다면 무시하시기 바랍니다.
(int *)(a+1) // a[1]
>>구현체가 변환 결과가 int 형에 적절히 정렬되지 않음을 알고
미리 그 값을 조정하지 않는 이상
아니면
(int *)(a+1) // a[2],
//구현체가 변환 결과가 int 형에 적절히 정렬되지 않음을 알고
미리 그 값을 조정한 경우
이렇게(위 코드의 주석처럼) 이해해도 되는 건가요?
아니면, 형변환으로 int 자료형 만큼 이동해야 한다고 생각해야 하나요?
변환 순간 정렬 제한 문제가 발생하면 그 행동은 undefined behavior
입니다. 즉, 그 어떠한 변화가 일어나도 무방합니다. 예를 들어,
(int *)(a+1) 변환 결과를 갑자기 널 포인터로 만드는 것조차 허용됩니다.
하지만, 제가 의도했던 것은 다음과 같은 상황입니다.
+-----------+-----------+-----------+-----------+
| short [0] | [1] | [2] | [3] |
+-----------+-----------+-----------+-----------+
| |
|
pi 가 가리킴
원래 이렇게 되어야 하는 상황인데, pi 가 가지게 될 주소(=가리키게 될
곳)이 int 형에 적절하게 정렬되어 있지 않다는 것을 알게 되어 이를 (예를
들어) 아래와 같이 다시 조정하는 것을 의미한 것입니다.
+-----------+-----------+-----------+-----------+
| short [0] | [1] | [2] | [3] |
+-----------+-----------+-----------+-----------+
| |
|
pi 가 가리킴
이 경우 *pi 에 접근하는 것이 정렬 제한으로 인한 문제를 일으키지 않지만
이를 다시 (short *) 를 통해 변환하는 경우 원래의 포인터 값 (a+1) 과
다른 값이 되는 결과가 발생합니다. 즉,
((short *)(int *)(a+1) != (a+1)) == 1
가 될 수도 있다는 의미였습니다.
물론, 저 역시 이와 같은 구현체를 본 적은 없습니다만, 정렬 제한 위반
시에 발생할 수 있는 전형적인 현상 중 하나도 빈번하게 언급되는
예입니다.
물론, 어떤 분들은 저렇게 포인터를 조정하는게 오히려 더 비용이 들지
않느냐고 반문할 수 있습니다만, 다음과 같은 경우를 생각해보면 꼭 그렇지
않다는 것을 알 수 있습니다.
"Gibralt 6600" (--;) 시리즈는 각 데이터형마다 서로 다른 포인터 표현을
사용하는 악명 높은 머신입니다. [*] int 형은 4의 배수 번지에만 올바르게
정렬되기에 int * 는 전체 32비트 중 2비트를 다른 용도(padding bit)로
사용합니다. 즉, 00000000 00000000 00000000 000001xx 는 xx 의 값에
무관하게 4번지를 나타내는 것입니다. 이 경우 short * 의 값을 int * 로
변환할 경우 모든 비트가 표현 변화 없이 그대로 오지만 xx 부분의 값이
무시되기 때문에 6번지를 나타내느 주소값(... 00000110)은 간단히 4번지를
나타내는 값(... 000001xx)으로 인식될 수 있습니다. 이때 두 비트의
padding 을 0 으로 설정하는 경우 이(... 00000100)를 다시 short * 로
변환하면 처음(... 00000110)과 다른 4번지가 되는 것입니다. 물론,
padding bit 가 0 이 아닌 다른 비트 패턴으로 설정될 경우 더 엉뚱한
결과가 되는 것도 가능합니다 (그래서 undefined behavior 입니다).
[*] 실제 다른 포인터형에는 동일한 표현을 사용하지만, char * 에 대해서
만큼은 독특한 포인터 표현을 사용하는 환경은 실존합니다 - 보통
word-addressed system 이 이에 해당합니다.
lovewar wrote:
많은 분들께 폐를 끼친것 같아 죄송합니다.
질문-답변 과정이 결코 폐가 되지는 않는다고 생각합니다. 그렇다면 저는
세계적으로 폐를 끼치는 사람이 되어 버립니다. --;
char data[0]; 과 char *data; 는 다른 의미입니다.
char data[0]; 과 char *data; 는 다른 의미입니다.
두 경우에 있어서 차이는 data 라는 이름의 차이가 생깁니다.
char *data 의 경우, data 의 값을 변경시킬 수 있습니다만,
char data[0] 의 경우, data 의 값을 변경시킬 수 없습니다.
아마 구조체의 마지막에 보신게 아닌가 생각합니다.맞나요? 그렇다는
아마 구조체의 마지막에 보신게 아닌가 생각합니다.
맞나요? 그렇다는 가정하에
저런 테크닉은
구조체 내에 data를 담는 크기를 계산하지 않고
바로 그 구조체 뒤에 따라나오는 데이터를 포인트하기 위해서 만듭니다.
보통
이런식으로 쓰입니다. 실제 data는 sizeof(struct header)에서 평가되지
않고 저렇게 가상의?? 멤버변수로서 데이터를 포인트 할 수 있게 만듭니다.
제가 알기론 C표준이 아니라 gcc익스텐션으로 알고 있고
char data[0]; 와 같은 statement는 문법위반인지 undefined behavior인지
확실치 모르겠네요 ^^
PS. 잘못된 부분이 있어 수정 합니다. ^^
C++, 그리고 C++....
죽어도 C++
[quote="ixevexi"]저런 테크닉은구조체 내에 data를
구조체 뒤에 따라나오는 데이터를 포인트 하려는 경우가 어떤 경우가 있나요? 일반적인 경우는 절대 아닐것 같은데요. 알려주세요.
감사합니다
먼저 답 주셔서 감사합니다.
구조체의 마지막에 있는 것이 맞습니다. ^^
다른 궁금한 점이 생겼는데요. sizeof(struct header)에 char data[0]가 평가되지 않는다고 하셨는데요. 그럼 어떤 값(여기서는 주소값)을 저장할 공간이 없다는 것인데 어떤 방법으로 포인터로써 사용될 수 있는지 궁금합니다. gcc에서 해결해 주는 꽁수(?)인가요...
Heading, heading again, again, ... ㅜㅜ 피난다
[quote="heoks"][quote="ixevexi"]저런 테크닉
바로 위에요 ^^
그냥
C++, 그리고 C++....
죽어도 C++
Re: 감사합니다
저도 초보라서 ^^ 자세한 구현내용은 모르나
다만 header의 바로 다음을 가르키기만 할듯 하니까 매우 쉬울듯 합니다. (항상 constant한 것처럼 보이자나요 header가 있을때 몇바이트 뒤니까요 ^^)
head.data = ~~ ; ==>
(char * ) head + sizeof( struct header) = ~~ 정도로 바뀌지 않을까요?
C++, 그리고 C++....
죽어도 C++
[quote="ixevexi"][code:1]struct he
head.data = ~~~~; 이건 error 아닌가요?
배열의 이름은 상수이기 때문에 문법에 어긋난 것 같습니다.
위에서 말씀하신 것처럼 다음 데이터를 포인팅 하는 것 같은데요.
정상적인 경우 다음 데이터의 주소는 위의 예에서 &a[1] 이라고 쓰면 되는데요.
gcc의 확장 기능입니다. [url=http://gcc.gnu.org/o
gcc의 확장 기능입니다. Arrays of Length Zero을 참고하십시오.
이와 관련된 글로서 C FAQs Q 2.6과 C FAQs Q 11.G이 있습니다.
[quote="doldori"]gcc의 확장 기능입니다. [url=htt
고맙습니다. :D
[quote="heoks"][quote="ixevexi"][cod
head.data = ~~~~;
이 문장은 그냥 급하게 든거구요 :oops:
머 함수에 인자로 넘긴다거나 할때 편하겠죠
가독성도 높아지고요 ^^
일반적으로 쓸때 편하다고 생각이 되네요
C++, 그리고 C++....
죽어도 C++
[quote="ixevexi"]아마 구조체의 마지막에 보신게 아닌가 생각
제가 알고있기로도 위에처럼의 용도로 많이들 사용합니다.
위의 테크닉은 단지 코드의 깔끔함때문에 쓰이는거 같진 않습니다.
대신 데이터의 연속성을 보장해주죠..
예를들면..
이와 같이 할수도 있고
와 같이 하는경우
전자에서의 header부분을 alloc하고 msg부분을 또 alloc해서 사용하지만 후자는 한번만 한다고 해서 좋은건 아니구요.
메모리의 안정성을 보면, 전자의 케이스인경우 header의 메모리가 어디에 저장되는지.. msg의 데이터가 어디에서 받아오는지 도통 알수가 없는경우가 생깁니다. 유저쪽에선 별문제될 소진 없으나 커널쪽에선 상당히 크리티컬한 버그로 번질수도 있는문제이겠죠.
그래서 이런이유로 후자의 경우와 같은 테크닉을 사용합니다.
"char msg[0]"이 gcc extension인건 오늘알았지만 이 extension이 제공되지 않는곳에서도 사용할려면 위와같이 "char msg[1]"로 두고 alloc할때 1바이트 차감하는식으로 사용합니다.
[code:1]struct mm{ int a; cha
로 vc++ 2005 에서 테스트 해봤는데
컴파일 에러가 나는군요...
vc에서는 안되나봅니다...
재미있어 보였는데 아쉽네요;;;
일하는 사람들의 희망 민주노동당 : http://www.kdlp.org
반공 교육의 성과로, 민주주의의 반대가 공산주의(또는 사회주의)라고 생각하는 사람이 많다.
[quote]vc++ 2005 에서 테스트 해봤는데 컴파일 에러가 나는군
다음의 방식으로 다시 해보시길 바랍니다. char data[0];은 gcc의 확장이고, C99의 방식은 char data[]; 입니다.
http://www.cinsk.org/cfaqs/html/node13.html#11.G
하지만 안되더라도 실망은 마십시오. C99를 100% 지원하는 컴파일러는 흔하지 않죠 :-)
마찬가지 컴파일 에러군요..
마찬가지 컴파일 에러군요..
일하는 사람들의 희망 민주노동당 : http://www.kdlp.org
반공 교육의 성과로, 민주주의의 반대가 공산주의(또는 사회주의)라고 생각하는 사람이 많다.
[quote="쌀밥"]마찬가지 컴파일 에러군요..[/quote]흠..
흠... VC++ 2005도 C99는 지원하지 않는가 보군요.
[quote="doldori"][quote="쌀밥"]마찬가지 컴파일 에러
VC++ 2003, 2005는 C99를 지원하나 완벽하게는 아닙니다.
제가 알기로는 80%~90%정도 지원하는걸로 알고 있습니다.
그리고 C99를 완벽하게 지원하는 컴파일러가 있긴 있나요?
[quote="doldori"][quote="쌀밥"]마찬가지 컴파일 에러
C++컴파일러지 C컴파일러가 아니지 않습니까?
(근데 C++표준에는 저게 없나요?)
[quote="매지"][quote="doldori"][quote="쌀밥"
[quote="욕심많은오리"][quote="doldori"]흠...
2005는 써보지 않아 모르겠고 2003은 C99를 전혀 지원하지 않습니다.
Comeau 정도면 완벽하게 지원한다고 할 수 있겠죠.
[quote="매지"][quote="doldori"]흠... VC++
제 글에 오해의 소지가 있었군요. ^^;
VC++ 2005에 있는 C 컴파일러라는 뜻이었습니다.
그리고 flexible array member는 C++ 표준에 없습니다.
flexible array member가 없다면, 좀 타협해서 다음은 어
flexible array member가 없다면, 좀 타협해서 다음은 어떻습니까?
[quote="Anonymous"]flexible array member
C FAQs Q 2.6의 세 번째 방법과 비슷하군요.
거기에 나온 제한 조건만 지킨다면 괜찮아 보입니다.
[quote]C FAQs Q 2.6의 세 번째 방법과 비슷하군요.
아래의 코드에서는 어떤 문제가 발생할 수 있을까요?
저는 T가 어떤 형이 되더라도, 아래의 3가지 이유로 문제가 없다고 생각합니다만... 틀린 부분 있으면 지적 바랍니다.
1. 일단, (char *)(ps+1)은 정렬제한을 만족합니다. 그렇지 않다면 S arr[3]의 두번째 요소는 정렬제한을 어기게 되겠지요.
2. 유효한 메모리 공간 바로 그 다음 공간을 가리키는 것은 유효합니다.
3. 모든 배열은 그 원소 사이사이에 padding을 둘 수 없습니다. 따라서 정렬제한을 어겨도 상관없는(즉 char형이나 char형만으로 이루어진 구조체같은) 타입이 아니라면 반드시 n(메모리의 정렬제한을 어기지 않는 숫자)의 배수 크기를 갖게 되어 있습니다. 만약 시작 주소가 유효한 주소라면, (시작주소+n*해당개체의 크기)를 해도 유효한 주소가 나와야 됩니다.
간단히 얘기해서, char형이 아니라도 문제가 없을 거라는 거죠. 어떻게
간단히 얘기해서, char형이 아니라도 문제가 없을 거라는 거죠. 어떻게 생각하십니까?
Re: char 포인터 질문
위와 같이 쓰는게 C에서는 허용 하나요? 그럼 좀 당황스럽네요.
Modern C++ Design에서 C/C++에서 동작하는 코드로 다음을 제시합니다.
#define STATIC_CHECK(expr) { char unnamed[(expr)?1:0];}
길이가 0인 배열은 허용하지 않기 때문에(!) 위의 코드는 컴파일 타임에 assertion 처럼 동작한다고 되어 있는데, 위처럼 사용하는게 맞다면 제대로 동작 안하겠네요.
https://wiki.ubuntu.com/KoreanTeam
Re: char 포인터 질문
0 이 정 문제가 된다면 음수로 -_-;
[quote="Anonymous"]간단히 얘기해서, char형이 아니라도
저도 무심코 넘겼는데 그런 지적도 나올 수 있겠군요. 저도 100% 확신할 수 없지만
일단 제 생각을 말씀드리겠습니다.
예를 들어 구조체 자체와 그 멤버인 int와 T*의 정렬 제한이 4의 배수이고 T형의
정렬 제한은 8의 배수인 경우에 정렬 제한 위반이 발생할 수 있음을 쉽게 알 수
있습니다. 이 구조체에 패딩이 없는 구현체라면 (ps + 1)은 4의 배수이지만
반드시 8의 배수라고는 할 수 없기 때문입니다.
이제 문제는 2배수 또는 4배수 등의 정렬 제한을 갖는 T형에 대해서도 정렬 위반이
발생할 수 있느냐는 것인데, 이건 저도 잘 모르겠습니다. 제가 접하는 환경에 길들여져서
사고의 틀이 경직된 것 같습니다. --;
이럴 때 전웅님이 출동하면 좋을 텐데...
Re: char 포인터 질문
구조체의 멤버가 아닌 배열 선언으로는 허용하지 않습니다. 원소의 개수는 반드시
0보다 커야 합니다.
지금 논의되고 있는 것은 배열의 선언이 아니라 이것이 구조체의 멤버로 있을 때
어떻게 되느냐 하는 것입니다. 위에서 말씀드린 gcc의 확장과 C FAQ의 링크를
참고하십시오.
Re: char 포인터 질문
그렇군요. 구조체 멤버로는 char some[0]이 저렇게 쓰일 수 있는 것이었군요.
https://wiki.ubuntu.com/KoreanTeam
[quote="doldori"]이럴 때 전웅님이 출동하면 좋을 텐데...
출동했습니다. --;
기본적으로 어떤 데이터형 X 의 정렬 제한을 align(X) 라고 놓겠습니다. 이
수는 1 이상의 정수가 될 수 있습니다. 물론, sizeof(X) % align(X) == 0
이어야만 합니다 - 그래야만 padding 없이 배열 요소가 모두 올바르게
정렬될 수 있습니다.
이제 malloc() 로 할당된 메모리의 정렬 제한에 대한 이야기부터
시작하겠습니다.
malloc() 가 반환하는 메모리에 가해지는 정렬 제한과 관련된 유일한
제약은, 그 *시작* 주소가 해당 구현체에서 가능한 모든 데이터형에 대해
정렬 제한을 만족해야 한다는 것입니다. 따라서,
int *p = malloc(sizeof(int)*n); // successful allocation assumed
에서 p 는 항상 올바르게 정렬되며, (p+i) 도 i <= n 인 이상 정렬 제한
문제를 겪지 않습니다. 하지만,
p = (int *)((char *)p + m);
에서는 m 이 align(int) 의 배수 (따라서 m % align(int) == 0) 가 아닌
이상 정렬 제한을 위반할 수 있다는 사실에 유의하셔야 합니다.
이제 언급한 문제로 돌아가 보겠습니다. "다른 데이터형을 모두 무시하고
예에 나온 데이터형만을 고려했을 때" malloc() 가 반환하는 메모리는
최소한 LCM(align(S), align(T), int) 에 정렬되는 메모리입니다. [*] 이제
위 코드가 정렬 제한 문제를 일으키는 경우는 sizeof(S) 가 align(T) 의
배수가 아닌 경우를 찾는 문제가 됩니다 (앞서 제가 보인 예와 비교해
보시기 바랍니다). 물론, 이와 같은 경우는 얼마든지 상상할 수 있지만,
여기서는 S 가 구조체라는데 특별히 주목할 필요가 있습니다. 구조체와
관련해서는 다음과 같은 사항이 덧붙습니다.
- 모든 구조체는 공통된 정렬 제한을 갖습니다. [**]
- 모든 (object) 데이터형이 구조체의 첫 번째 멤버로 선언될 수 있습니다.
- 구조체의 가장 앞 부분에는 padding 이 붙지 않습니다.
따라서 구조체는 사실상 malloc() 가 반환하는 메모리의 정렬 제한을
가져야 하고, 결국, (ps+1) 역시 동일한 정렬 제한을 만족하게 되므로
(T *) 데이터형으로의 변환에 아무 문제가 없게 됩니다.
여기까지가 실제 implementation 에서 일어나는 이야기입니다. 이제 표준에
관심있는 분들을 위해 엄격한 표준의 관점으로 들어가 보도록 하겠습니다.
동일한 데이터형이라고 해도 구조체 멤버일 경우와 그렇지 않은 경우 정렬
제한이 달라질 수 있습니다. 즉, 포인터를 위한 "일반화된 정렬 제한"이
존재하고 구조체 안팎에서 서로 다른 정도로 정렬 제한을 강요하는 것이
가능합니다.
예를 들어, 어떤 데이터형 Y 가 12 바이트 크기를 갖고, 데이터형 Y의
대상체가 구조체 멤버인 경우에는 align(Y) = 4, 구조체 멤버가 아닌
경우에는 align(Y) = 6 이라고 가정하겠습니다. 이 경우 12 % 4 == 0,
12 % 6 == 0 이므로 배열과 관련된 문제는 없습니다. 이때 포인터는 구조체
안팎의 대상체를 모두 가리킬 수 있어야 하므로 "일반화된" 정렬 제한은
GCD(4, 6) = 2 가 됩니다. 하지만, 데이터형 Y의 구조체 안팎의 정렬
제한을 생각하면 모든 짝수가 올바른 (Y *) 타입의 포인터 값은 아니라는
것을 알 수 있습니다. 즉, 설명을 위해 주소 공간이 linear 한 경우를
생각해보면 2나 10의 경우 분명 짝수이지만, 해당 번지에는 결코 데이터형
Y의 대상체가 올 수 없음을 쉽게 알 수 있습니다. 이제 이와 같은 경우
(즉, sizeof(S) 가 "일반화된" 정렬 제한의 배수가 아니거나 유효하지 않은
배수가 되는 경우)가 문제로 주어진 경우에도 발생할 수 있는지 생각해
보겠습니다.
구조체 멤버일 때의 정렬 제한을 Salign(), 일반화된 정렬 제한을 Galign()
이라고 부르겠습니다. 지금까지의 내용을 바탕으로 요구되는 관계를
정리하면 다음과 같습니다.
(1), (2), (3) 은 당연한 이야기입니다. (5), (6) 은 "일반화된" 정렬
제한을 구하는 과정에 의한 것이며, (4) 는 S 가 구조체이고, 이 구조체의
정렬 제한은 "최소한" 데이터형 T의 멤버를 첫 번째 멤버로 갖는 다른
구조체와도 같아야 하기 때문에 주어진 조건입니다. 배수 관계는
transitive 하기 때문에 이 조건을 정리하면 sizeof(S) 는 항상 Galign(T),
Salign(T) 의 배수가 됨을 알 수 있습니다. 하지만, sizeof(S) 가 반드시
align(T)의 배수가 될 필요는 없다는 것도 알 수 있습니다. 단, 이미
언급했듯이 앞서 살펴본 경우와 달리 이 경우에는 sizeof(S) 의 배수가
항상 유효한 Galign(T) 의 배수임만을 확인하면, sizeof(S)와 align(T)의
관계는 중요하지 않습니다. sizeof(S) 가 항상 Salign(T) 의 배수이고,
Salign(T) (의 배수)는 항상 유효한 Galign(T) 의 배수이기 때문에 결국
sizeof(S) 가 항상 Galign(T) 의 유효한 배수임을 확인할 수 있습니다.
따라서 엄격한 표준 해석을 적용해도 문제를 일으키지 않습니다.
[*] malloc() 가 정렬 제한을 보장하는 방법에는 모든 type 에 정렬될 수
있는 주소를 취하는 것 이외에도 malloc() 로 할당되는 메모리의 정렬
제한을 char 수준으로 완화되는 것도 가능합니다. 하지만, 이 경우는
제기된 문제를 압박하는 경우가 되지 않으므로 고려하지 않습니다.
[**] 구조체의 정렬 제한과 관련된 문제는 C99에 새로 추가된 요구입니다.
하지만, opaque type 이 작동하기 위해서는 C90 에서도 사실상
있었어야 하는 요구이므로, 모든 표준이 사실상 이를 요구하고 있다고
보아도 무방합니다.
그럼...
p.s. 절 미쳤다고 생각하진 마시고, 그냥 이런거 하면서 재미느끼는 사람도
있구나 정도로 생각해 주시기 바랍니다. --;
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
자세한 설명 감사합니다. 그런데 좀 의문이 있습니다.[quote="전
자세한 설명 감사합니다. 그런데 좀 의문이 있습니다.
malloc()이 반환하는 메모리는 LCM(align(S), align(T*), int)에 정렬되는 것이
아닌지요? (align(T)가 아니라). 만약 align(T)와 align(T*)가 무관하다면
(ps+1)이 T형의 정렬 제한을 항상 만족한다고 할 수는 없지 않나요? T가 char인
경우에는 항상 만족하겠지만요.
표준을 엄격하게 적용했을 때의 설명도 구조체의 멤버가 T형일 때 적용되는 것이
아닌가 합니다. 멤버가 T* 형일 때는 달라지지 않나요?
[quote="doldori"]자세한 설명 감사합니다. 그런데 좀 의문이
그러고 보니 잘못 적었군요. 의도한 바는 LCM(align(S), align(T),
align(int), align(T *)) 정도가 되겠네요. 보다 정확히 적자면,
LCM(align(S), align(T), align(int), align(T *), C 언어의 정렬 제한이
다른 가능한 모든 타입...) 가 되어야 합니다. 따라서 align(T) 냐
align(T *) 냐는 답과는 상관 없습니다.
아닙니다. 일단 ps 가 모든 type 에 대해서 정렬이 되어야 하므로, (ps+1)
역시 모든 type 에 대해서 정렬이 되어야 합니다.
sizeof(S) % align(S) = 0 이라는 점을 적용해 보시기 바랍니다.
적혀 있듯이, 구조체의 첫 번째 멤버로 T 형을 갖는 구조체와도 문제 없는
정렬 제한을 가져야 합니다. 물론, 첫 번째 멤버로 T * 형을 갖는
구조체와도 문제가 없어야 하구요. 문제를 해결할 때 중요한 것은 첫번째
멤버로 T 형을 갖는 구조체이기에 그것만 고려했을 뿐입니다. T * 형을
계산에 추가하는 것이 답에 영향을 주지는 않으며, T 형을 빼는 것은
불가능합니다.
그럼...
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
네, 보충설명 감사합니다. 제가 구조체와 정렬에 대해서 많은 부분을 잘못
네, 보충설명 감사합니다. 제가 구조체와 정렬에 대해서 많은 부분을 잘못 알고
있었군요. 그럼 C FAQ 2.6의 내용은 고쳐야겠는데요.
이것을 Steve Summit씨에게 알려준다고 해도 그냥 고치지는 않을 것이고
먼저 clc에서 논의가 돼야 할 텐데, 영어로 이런 긴 얘기를 꺼낸다는 것부터가
어렵겠는데요. -_-; cinsk님은 어떻게 생각하시나요?
[quote="doldori"]네, 보충설명 감사합니다. 제가 구조체와
사실 위에서 언급했듯이 모든 구조체가 같은 정렬 제한을 가져야 한다는
요구가 없으면 (증명 과정에서 가장 중요한 근거가 사라지게 되므로)
문제의 코드는 당연히 정렬 제한 문제를 겪으며, C FAQ 2.6 역시 고칠
필요가 없습니다.
하지만, 그와 같은 (사실상 C90 에도 내재되어 있던) 요구가 C99 에
명시적으로 추가되었기 때문에 가시적인 차이가 발생하게 되었습니다.
사실상 struct hack 에 대한 해결책을 제시하기 위해 추가된 부분이 아닌데
본의 아니게 그와 같은 역할을 할 수 있게 된 것 같습니다.
매우 pedantic 한 입장에서 제가 마지막으로 의심을 갖는 부분은 "모든"
구조체가 동일한 정렬 제한을 가져야 한다는 요구 조건이 "해당 구현체에서
가능한 모든 구조체"에 적용되는 것인지, 아니면 "어떤 프로그램에
존재하는 모든 구조체"에 적용되는 것인지가 불분명합니다.
문제가 되는 부분이 일반적인 type 을 정의하고 설명하는 부분이기 때문에,
또한 그와 같은 요구가 도입된 가장 큰 배경이 opaque type 지원이기
때문에, 이론적으로나 현실적으로나 "해당 구현체에서 존재하는 모든
구조체"에 적용되는 것으로 보는 것이 타당한 것 같습니다만, 제가
유권해석을 내릴 수 있는 입장이 아니라 장담은 못합니다. 이 부분은 곧
csc 에서 한번 논의해보려 합니다 - 만약, 해당 논의에서 제 주장과 동일한
결론이 나오는 경우 Steve Summit 씨께는 해당 논의 링크를 전달하는 것
만으로도 충분할 것입니다.
하지만, C FAQs 의 성격을 보았을 때 수정이 이루어지지 않을 가능성이
크다고 봅니다. 질문 2.6 이외에도 이미 다른 몇몇 부분에서 설명을 쉽게
하기 위해 잘못된 설명을 한 부분이 있지만, 실용적인 프로그래밍에서는
크게 영향을 미치지 않는 부분들입니다. 이 부분 역시 이미 C99 에서 FAM
를 지원하는 상황에서 그와 같은 설명을 굳이 수정, 추가할 이유는 그리
커보이지 않습니다.
그럼...
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
이 부분과 관련해 기본적으로 제 치명적인 실수가 있었으며, 아직 단정하기
이 부분과 관련해 기본적으로 제 치명적인 실수가 있었으며, 아직 단정하기
어려운 부분이 많습니다. 현재 틈날 때마다 관련된 문제를 고민, 정리
중입니다.
저 위에 주저리 주저리 적은 답변은 일단 무시하시기 바랍니다.
확정적인 어조로 잘못된 답변 드린 점 사과드립니다.
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
흠... 이 문제로 처음 생각했던 것보다 한참을 고민하게 되었습니다.
흠... 이 문제로 처음 생각했던 것보다 한참을 고민하게 되었습니다.
덕분에 제가 잘못 알고 있던 부분도 확실히 고쳐 알 수 있는 계기가
되었습니다.
저 위에 틀린 내용들이 많이 있지만, 겉으로는 번지르해 보이는 논리가
틀릴 수도 있다는 사실을 알리고자(--;) 굳이 편집하거나 지우지 않고
아래에 새로 답을 달도록 하겠습니다.
우선, 중요한 내용 하나를 정정하자면,
C90에 이미 암시되어 있고, C99에 명시적으로 추가된 구조체와 관련된 정렬
제한 내용은, 구조체 자체가 아닌 구조체 포인터에 대한 정렬 제한 이야기
입니다. 오랜만에 표준 인쇄본을 펼치니 해당 페이지에 "구조체가 아닌
포인터에 대한 이야기임!"이라고 메모가 되어 있는데도 정성을 덜 들인
탓인지 금새 잊어버리고 말았네요. 따라서 모든 구조체가 반드시 같은 정렬
제한을 가질 필요는 없습니다. 이런 사실을 바탕으로 앞서 말씀드렸던
잘못된 답변을 올바르게 고쳐 정리하겠습니다.
기본적으로 어떤 데이터형 X 의 정렬 제한을 align(X) 라고 놓겠습니다. 이
수는 1 이상의 정수가 될 수 있습니다. 물론, sizeof(X) % align(X) == 0
이어야만 합니다 - 그래야만 padding 없이 배열 요소가 모두 올바르게
정렬될 수 있습니다.
이제 malloc() 로 할당된 메모리의 정렬 제한에 대한 이야기부터
시작하겠습니다.
malloc() 가 반환하는 메모리에 가해지는 정렬 제한과 관련된 유일한
제약은, 그 *시작* 주소가 해당 구현체에서 가능한 모든 데이터형에 대해
정렬 제한을 만족해야 한다는 것입니다. 따라서,
int *p = malloc(sizeof(int)*n); // successful allocation assumed
에서 p 는 항상 올바르게 정렬되며, (p+i) 도 i <= n 인 이상 정렬 제한
문제를 겪지 않습니다. 하지만,
p = (int *)((char *)p + m);
에서는 m 이 align(int) 의 배수 (따라서 m % align(int) == 0) 가 아닌
이상 정렬 제한을 위반할 수 있다는 사실에 유의하셔야 합니다.
이제 언급한 문제로 돌아가 보겠습니다. "다른 데이터형을 모두 무시하고
예에 나온 데이터형만을 고려했을 때" malloc() 가 반환하는 메모리는
최소한 LCM(align(S), align(T), align(int), align(T *), ...) 에
정렬되는 메모리입니다. [*] 이제 위 코드가 정렬 제한 문제를 일으키는
경우는 sizeof(S) 가 align(T) 의 배수가 아닌 경우를 찾는 문제가 됩니다
(앞서 제가 보인 예와 비교해 보시기 바랍니다). 이와 같은 경우는 충분히
상상해 볼 수 있기 때문에 해당 코드가 정렬 제한 문제를 일으킬 수 있다는
사실은 쉽게(비록 전 쉽게 알지 못했습니다만) 알 수 있습니다.
이제 조금 더 복잡한 경우로 구조체 S 가 다음과 같은 선언된 경우를
생각해 보겠습니다. (솔직히 저 위에서 멈추기에는 민망해서 예 하나 더
추가합니다 ;-)
첫 번째 멤버로 T dummy; 를 넣은 이유는 구조체 S 의 padding 을 결정할
때 이 구조체 타입의 배열이 고려되어야 하기 때문에 foo 는 물론, foo+1
역시 T 형에 올바르게 정렬된다는 사실을 얻기 위해서 입니다.
물론, 이와 같은 생각 아래에는 다음과 같은 가정이 깔려 있습니다.
- p 가 데이터형 T 에 대해 올바르게 정렬되어 있고 p 가 가리키는 곳에
데이터형 T 의 대상체가 존재하는 경우, p+1 역시 항상 데이터형 T 에
대해 올바르게 정렬된다.
실재하는 구현체를 고려했을 때 이 가정이 맞지 않는 구현체는 없다고 확신
합니다. 따라서 처음 주어진 코드에 T dummy; 를 첫번째 멤버로 추가해
주는 경우 실제 구현체에서는 정렬 제한 문제를 겪지 않습니다.
하지만, 제가 적은 가정이 표준에 의해서 명백하게 보장되지는 않습니다.
표준에 의하면 정렬(alignment)은 대상체(object)에 대해 정의되어
있습니다. 즉, 대상체가 존재하지 않으면 정렬 역시 존재하지 않습니다.
따라서,
이와 같은 코드는 정렬 제한 문제를 겪지만,
이와 같은 코드는 정렬 제한 문제를 겪지 않습니다. 널 포인터 자체가
"아무 것도" 가리키지 않는 포인터이기 때문에 대상체가 관여할 수 없고
결국 정렬 제한이 적용될 수 없기 때문입니다.
하지만,
이 코드에서 foo+1 의 의미에는 다소 문제가 있습니다. 분명, foo+1 이
어떤 대상체를 가리키는 것은 결코 아니지만, 그렇다고 널 포인터마냥 아무
것도 가리키지 않는다고 할 수도 없는 상황이기 때문입니다. 이 부분에
대해서 표준이 침묵하고 있기 때문에 foo+1 의 정렬에 대한 가정이
만족된다고 확신할 수는 없습니다만, foo+1 의 정렬 제한이 만족됨을
보장해주지 않을 경우 매우 무의미하며 이상한 결론으로 귀결되기 때문에
필히 그 의도는 foo+1 의 정렬을 보장해주는 것이라 믿고 있습니다.
물론, 이와 같은 흐름의 해석에서는 첫번째 멤버로 데이터형 T 의 대상체가
나왔기 때문에 더 이상 구조체 안팎에서 다른 정렬 제한을 갖는 이론적
이야기는 다룰 필요가 없습니다 - 하지만, 몇몇 분들에게는 흥미로울 수
있는 이야기라 생각하기에 삭제하지 않았습니다.
[*] malloc() 가 정렬 제한을 보장하는 방법에는 모든 type 에 정렬될 수
있는 주소를 취하는 것 이외에도 malloc() 로 할당되는 메모리의 정렬
제한을 char 수준으로 완화되는 것도 가능합니다. 하지만, 이 경우는
제기된 문제를 압박하는 경우가 되지 않으므로 고려하지 않습니다.
역시 까다로운 문제를 건드리기 시작하면 어김없이 표준의 "어두운"
부분으로 들어가게 되어있나 봅니다. 처음엔 자신만만하게 답변을 드리려는
의도였는데, 결과적으로 저 역시 많은 것들을 배워 갑니다. 감사합니다.
그럼...
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
[quote="전웅"]하지만, 제가 적은 가정이 표준에 의해서 명백하
메모리(Stack) 영역에
int 형
Pointer 형
Pointer 형
이렇게 정렬(?)될 것 같은데,
정렬 제한 문제를 겪는다고 하셨는데, 설명좀 부탁드립니다.
[quote]정렬 제한 문제를 겪는다고 하셨는데, 설명좀 부탁드립니다.[
align(int) % align(short) != 0 일때 문제가 생깁니다. 간단하게 int형이 6바이트의 정렬제한을 가지고 있고 short형이 4바이트의 정렬제한을 가지고 있다면, int형의 주소가 6인 경우 이것을 short *ps에 대입하면 문제가 생길 수 있습니다.
malloc 구현은 하드웨어 의존적입니다. 그러므로 정렬이 필요한하드
malloc 구현은 하드웨어 의존적입니다. 그러므로 정렬이 필요한
하드웨어 시스템이라도 항상 malloc 이 리턴하는 주소가 C 에서
지원하는 모든 타입의 최소 공배수가 되지는 않습니다.
예를들어 대개의 하드웨어는 FPU 에서 double 계산을 지원하지만
long double 계산은 지원하지 않습니다. 이런 경우 long double
은 소프트웨어 루틴에 의해 계산되고 정렬 제한이
sizeof(long double)이 아닐 수가 있습니다. 이런 시스템인 경우
long double 은 최소 공배수 계산에서 배제될 수 있지요.
아무튼 지원되는 모든 타입의 최소 공배수를 옵셋으로 하면
모든 정렬 문제는 피할 수 있습니다. 최적으로 피하는 건 아니지만요.
그리고 위에서 언급된 코드
는 다음과 같이 수정하면 될 것 같습니다.
결국 옵셋을 sizeof(T) 로 정렬하는 것입니다.
Orion Project : http://orionids.org
[quote="Anonymous"]flexible array member
flexible array member 는 C 에 항상 있습니다. 단지 [0] 이나 []
처럼 선언하는 것을 지원하지 않을 뿐, 배열 크기로 1 이상의
값 (대개는 1)을 주고 malloc 할 때 잘 조절해주면 되니까요.
그리고 C++ 표준에는 flexible array member 가 결코 들어갈 수
없습니다. new 를 오버로딩하지 않는한 절대 불가능하니까요.
Orion Project : http://orionids.org
[quote="Anonymous"][quote]정렬 제한 문제를 겪는다고
맞는 말 같습니다.
하지만, 예제에 있는 내용은 short형이 아닌 pointer 형이란 점에서
차이가 있지 않을까하는 생각입니다.
[quote="체스맨"]malloc 구현은 하드웨어 의존적입니다. 그러므
물론입니다. 하지만, 위에서 OP 께서 보여주신 코드를 다룰 때 "설명이
용이하면서도 가장 압박할 수 있는" 환경은 바로 malloc() 가 반환한
메모리의 정렬 제한이 모든 type 의 정렬 제한의 최소 공배수가 되는 경우
입니다. "[*]" 부분에서 말씀드렸듯이 가장 완화되는 경우는 malloc() 가
반환하는 메모리 영역에 아예 정렬 제한이 요구되지 않는 경우입니다.
체스맨님께서 아래 언급해주신 상황은 그 두 극단 사이에 있는 상황입니다.
두 극단이든 그 중간의 한 상황이든, 어떠한 경우에도 malloc() 가
반환하는 메모리는 모든 type 에 대해 정렬 제한 문제를 겪지 않아야
합니다. 또한, malloc() 가 반환한 메모리가 특정 type 의 배열로 사용될
경우를 고려해 (type *)"malloc'ed storage" + n 역시 정렬 제한 문제를
겪지 않아야 합니다. 이와 같은 요구를 반영해 정렬 제한 문제를
설명하고자 할 때 alignment modulus 를 사용한다면 LCM(...) 가 가장
적절합니다 (그렇게 하지 않으면 일반화된 해석을 얻기 어렵습니다).
C 언어의 입장에서는 double 과 long double 이 type algebra 를 제외하면
동일하게 다루어지는 type 일 수 있기 때문에 여기서는 "long double" 로
IEEE 754 의 extended (double) precision 을 의미하시는 것으로
이해하겠습니다.
"대개의 하드웨어"에 x86 계열이 포함되지 않는다면 "대개의 하드웨어"가
long double 계산을 지원하지 않습니다. x86 및 그 클론은 extended-based
system 의 대표적인 예입니다. 즉, IEEE 754 의 single precision/double
precision 을 full support 하지 않으며, double-extended precision 을
full support 하고 있습니다.
여기서 "최적이 아니다"란 말씀은 "불필요하게 엄격한 부분이 있다" 는
의미로 이해하겠습니다. 언급했듯이 malloc() 가 반환하는 메모리에
가해지는 요구가 alignment modulus 로 기술되어 있지는 않습니다. 따라서
각 구현체마다 다양한 방법으로 표준 요구를 만족할 수 있습니다. 예를 들어,
다음과 같은 경우도 가능합니다.
char 를 제외한 모든 type 의 "일반화된" 정렬 제한은 2 입니다. 단,
sizeof(type) >= 4 인 모든 type 은 지역 선언에 의해 선언된 type 일 경우
에는 성능상의 이유로 sizeof(type) 을 정렬 제한으로 갖습니다. 따라서
align(double) == 8 이며, align(long double) == 10, align(int) == 4
등이 됩니다. 하지만, 말씀하신 "최적"에 가깝게 가기 위해 malloc() 를
통해 할당된 메모리 영역의 정렬 제한이 2 가 될 수 있습니다. 이 경우
align(malloc'ed storage) % 4 != 0 && align(malloc'ed storage) % 8 != 0
이어도 "일반화된" 정렬 제한이 2 이기에 실제적인 정렬 제한 문제를 겪지
않습니다.
혹은 좀 더 그럴싸한 예로, sizeof(type) >= 4 인 모든 type 의 정렬
제한은 4 입니다. 따라서 sizeof(type) % 4 == 0 인 type 은 큰 문제를
겪지 않지만, sizeof(type) % 4 != 0 인 type 은 문제가 될 수 있습니다.
따라서 long double 에 맵핑되는 extended precision 은 IEEE 754 를 따라
실제 10 바이트이지만, 이와 같은 정렬 제한 문제 때문에 long double 에
2 바이트의 패딩을 담습니다 - 특정 환경이 번뜩 떠오르는 분들도 계실
겁니다.
정렬 제한과 관련된 많은 부분은 implementation-defined 입니다. 저는
줄곧 흔히 "alignment modulus" 라고 부르는 방법으로 이를 설명하고
있지만 위원회 멤버 중에는 이런 방식의 설명을 못마땅하게 생각하는
사람도 있습니다 - 하지만, 최소한 "alignment" 의 정의가 이를 부분적으로
지지하고 있다고 생각합니다. 하지만, 이미 "일반화된 정렬 제한"의 예에서
볼 수 있듯이 "align(type) == 단일 정수" 의 단순한 모델은 적용할 수
없음은 분명합니다.
예, 가능한 방법입니다. 하지만, C99 이전에는 가급적 struct hack 을
위해서 그야말로 "hack" 을 쓰지 않기를 추천합니다. 굳이 hack 을 써야
한다면 C90 표준의 지원 여부를 떠나 C99 의 FAM 과 유사한 형태로
사용하시길 추천합니다 - 어차피 C FAQs 에서 C90 표준이 지원하지
않는다는 두 형태가 실제 작동하지 않는 환경은 찾기 어렵습니다. 즉, 추후
C99 환경으로 이전할 때 가능한 수정이 적은 방향으로 작성하는 것이
유리합니다. 이때 다른 부분은 큰 문제를 일으키지 않지만 sizeof(구조체)
에 유의하셔야 합니다. C99 에서 FAM 이 포함된 구조체의 경우 정렬 제한
만족을 위한 padding 만 크기에 고려될 뿐, 실제 FAM 은 포함되지
않습니다.
그럼...
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
[quote="체스맨"][quote="Anonymous"]flexible
의도는 이해하겠습니다만 다소 오해의 여지가 있습니다. FAM 은 C99
이전에는 없습니다. FAM 은 이제 C99 에 추가된 새로운 feature 를
지칭하는 용어입니다.
C FAQs 에서 보인 흔히 "struct hack" 이라고 부르는 트릭은 C99 의 FAM 이
도입된 이후부터는 FAM 이라고 부르기 부적절합니다.
애초 "struct hack" 의 대표적인 2가지 방법이 문제가 되는 것은 표준이
해당 코드 모두를 정의되지 않은 것으로 명시했기 때문입니다 - 이것이
C99 에 FAM 이 도입된 계기입니다.
이와 같은 내용은 doldori 님이 인용해 주신 두 링크(C FAQs)에 잘
설명되어 있습니다.
그렇지 않아도 복잡함이 극에 달하는 C++ 표준에 FAM 이 무리 없이
들어가기는 결코 쉽지 않다는 사실에 동의합니다. 하지만, 가능한 C/C++ 의
차이를 줄여 컴파일러를 개발하는 것이 유리한 개발사 입장에서는 확장을
통해 C++ 에서도 C99 의 기술을 지원하고 있습니다 - 물론, 실질적으로
지원을 하는 것과 언어 정의에 문제 없이 포함되는 것에는 엄청난 차이가
있을 수 있습니다.
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
[quote="lovewar"][quote="Anonymous"][quo
정렬 제한과 관련해 약간의 오해가 있는 것 같습니다. 지극히 이상적인
상황이지만 다음과 같은 경우를 생각해 보겠습니다.
1. 메모리의 addressing system 으로 linear address space 를 가정합니다.
즉, 바이트 단위로 0 부터 메모리의 끝까지 연속적인 주소 공간이
형성되고, 포인터의 값은 바로 그 주소값을 나타냅니다. 예를 들어,
포인터가 12번지에 있는 대상체를 가리킨다면 그 포인터의 값은 정수로
변환했을 때 12가 됩니다.
2. 특정 type T 에 대한 정렬 제한을 간단히 "align(T) == 단일 정수"로
표현할 수 있다고 가정합니다.
3. sizeof(short) == 2, sizeof(int) == 4 를 가정합니다.
4. align(short) == 2, align(int) == 4 를 가정합니다.
이때 align(short) == 2 란 이야기는 short 데이터형의 대상체는 모두 2의
배수 번지에 놓여야 한다는 것을 의미하며, 유사하게 align(int) == 4 란
이야기는 int 데이터형의 대상체가 모두 4의 배수 번지에 놓여야 한다는
것을 의미합니다. 따라서 short 데이터형의 포인터의 값은 항상 2의 배수가
되어야 하며, int 데이터형의 포인터는 항상 4의 배수가 되어야 합니다.
포인터 변환시 정렬 제한 문제가 만족되어야 한다는 것은, int * 는 항상
4의 배수 값을 가져야 하며, short * 는 항상 2의 배수 값을 가져야 한다는
것을 의미합니다. 물론, 일부 계열에서는 정렬 제한이 만족되지 않는 경우
성능 저하 정도로 끝나지만, 다른 환경에서는 프로그램 강제 종료까지 만날
수 있습니다.
이제 제가 처음에 든 예를 (이론상 잘못된 것은 아니지만 현실적으로
부적절한 면이 있으므로) 다음과 같이 바꾸겠습니다.
이때 arr 이 12번지에 할당되었다고 가정하겠습니다. 이 경우 &arr 의 값은
12가 되며, 12 는 2의 배수인 동시에 4의 배수도 될 수 있기 때문에 int *
의 값이 되는데 아무런 문제가 없습니다.
하지만, arr 이 14번지에 할당되는 경우 short * 로는 문제가 없지만 int *
가 가져서는 안 되는 값이 되기 때문에 정렬 제한 문제를 일으킬 수
있습니다.
실제 예를 확인해 보면 다음과 같습니다.
여기서 한가지 유의하실 부분은 포인터 변환시 정렬 제한을 어기게 되면
바로 변환이 이루어지는 시점부터 정의되지 않은 행동이 된다는 것입니다.
즉, 실제로는 위의 예처럼 직접 해당 메모리에 접근하려 할 때 문제가 되는
경우가 대부분이지만, 이론상으로는 (int *)(ps+1) 에서 프로그램이 오작동
하는 것도 가능합니다. 직접 본 적은 없지만 실제 정렬 제한을 만족하지
않는 변환일 경우 구현체 차원에서 포인터 값을 조정해 정렬 제한을
만족하도록 해줄 수도 있다고 합니다. 이 경우 (short *)(int *)(ps+1) 가
원래 포인터인 (ps+1) 과 다른 값이 되는 결과가 나올 수도 있습니다.
그럼...
p.s. 추운 날씨에 건강 조심하시기 바랍니다. ^^
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
[quote="전웅"]정렬 제한과 관련해 약간의 오해가 있는 것
좋은 설명 감사합니다.
예제로 설명하신 내용들이 정렬제한과 관련해서 좀 어울리지
않는것 같습니다.
정렬제한 문제는 서로 다른 자료형들이 위치할때
발생하는 문제로 생각됩니다만(개인적인 생각),
-- 위 문장은 잘못되었기에 정정합니다.. --
나중에 설명하신 예제들은 연속적인 메모리를 확보하고 그중에서
데이타를 참조(케스팅)할때의 예제라서 이해하기 힘듭니다
-- 여기서도 정렬 제한 문제로 인한 원치 않는 데이타 값을
가져올 수 있다는 것을 잠시 잊고 있었습니다. --
혹시, 연속된 메모리를 물리적 상태의 연속적인 메모리가
아니라 논리적 상태의 연속적인 메모리라 가정한다면
정렬문제가 발생할 수 있을것 같습니다.
[quote]정렬제한 문제는 서로 다른 자료형들이 위치할때발생하는 문
아직도 정렬제한이 뭔지 제대로 이해 못하셨으니 다시 한번 읽어보십시오.
[quote]-- 여기서도 정렬 제한 문제로 인한 원치 않는 데이타 값을
아직도 제대로 이해 못하셨습니다. 정렬제한이 존재하는 CPU라면 아예 읽지 못하는게 정상입니다.
[quote="Anonymous"][quote]-- 여기서도 정렬 제한
정말인가요?
정렬제한이 메모리의 공간에 정렬하는 것 이상으로 다른 뭔가가 있나요?
좋은 자료있으면 부탁드립니다.
[quote="lovewar"]좋은 설명 감사합니다.예제로 설명하
정렬 제한은 메모리 상의 모든 대상체에 적용되는 개념이며, 정렬 제한이
문제를 일으킬 수 있는 대표적인 경우가 포인터 변환과 구조체 패딩입니다.
정렬 제한은 "특정 type"을 갖는 "대상체"가 "특정 위치"에 놓여야 한다는
요구입니다. 따라서 앞앞앞에 말씀드린 예에서처럼 그 엄격함의 차이가
있을 순 있지만, malloc()로 할당된 대상체든 일반적으로 선언된 변수든
기본적으로 char 를 제외한 모든 type 의 대상체에 적용됩니다 - char 는
그 type 의 특성으로 인해 항상 가장 덜 엄격한 정렬 제한을 갖습니다.
물론, 다음과 같은 경우도 충분히 가능합니다.
제가 위와 같은 코드를 사용하지 않고 malloc()로 할당한 메모리를 예로
든 이유는 정렬 제한이 아닌 다른 이유로 undefined behavior 가 관여하지
않도록 하기 위해서입니다.
이미 말씀드렸듯이 표준 관점에서는 포인터 변환이 이루어지는 시점 부터
정렬 제한으로 인한 undefined behavior 가 발생하지만, 실제 구현체에서
그 영향(bus error)을 가시적으로 확인하기 위해서는 변환된 포인터 값을
사용해 실제 메모리를 접근(예를 들면, *pi = 0)할 필요가 있습니다.
그런데, 위 두 코드 모두에서 *pi = 0; 을 할 경우 정렬 제한이 아닌 다른
문제(표준에서는 흔히 "object problem" 이라고 부릅니다)로 undefined
behavior 가 관여하게 됩니다. 따라서 그와 같은 가능성을 철저히 배제하고
순수하게 정렬 제한 문제로만 발생하는 undefined behavior 의 영향을
보이기 위해 처음 설명과는 달리 malloc() 로 할당한 메모리를 사용한
것입니다.
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
[quote="전웅"]이미 말씀드렸듯이 표준 관점에서는 포인터 변환이
설명 감사합니다.
이해했습니다.
-- 덧붙이는 글 --
넘 무지해서 예제를 이해하는데 시간이 걸렸습니다.
예제 작성시 특정부분만을 설명하는 예제를 사용해 주시면 감사합니다.
저 같은 초보를 위해서
제가 이해했던 것을 도형으로 그립니다.
[quote="lovewar"][quote="Anonymous"][quo
저 위에 purple 서버에서 실행한 모습은 실제 머신(SunOS)에서 실행한
결과입니다. 보시면 아시겠지만, 정렬 제한 문제로 프로그램이 강제
종료되고 있습니다.
정렬 제한 문제는 C FAQs 비롯해 일부 서적에서 어느 정도 다루고 있다고
생각합니다. 찾아보니 제 게시판에는 다음과 같은 내용이 있습니다.
http://www.woong.org/board/?doc=bbs/gnuboard.php&bo_table=hclc&wr_id=653
http://www.woong.org/board/?doc=bbs/gnuboard.php&bo_table=hclc&wr_id=754
정렬 제한은 보통 해당 환경의 CPU 의 능력과 효율 등과 관련된 경우가
많습니다. 설명을 위해 이번에도 단순한 설명을 위해 가상의 예를
들겠습니다.
"Gibralt 7000" (--;;) 은 32-bit CPU 를 탑재한 기종으로 메모리에서
4 바이트(32 비트) 단위로 읽어오는 명령어만을 지원합니다. 단, 임의의
번지에서 4 바이트가 아니라, 0, 4, 8, ... 4n 번지에서 4 바이트씩 읽어
오도록 구성되어 있습니다. 이 경우 3번지에 놓인 문자형 자료를 읽어올
경우 0번지부터 4바이트를 읽은 후에 적절히 shift 와 bit-masking 을 해
원하는 바이트를 얻게 됩니다.
이제 이 문맥에서 앞서 보인 예를 살펴보겠습니다.
short a[4];
이는 메모리 상에 다음과 같이 구성된다고 가정하겠습니다.
그리고 a[0] 의 주소는 0 이라고 가정하겠습니다. 이제
int *pi = (int *)(a+1);
을 실행하면 (구현체가 변환 결과가 int 형에 적절히 정렬되지 않음을 알고
미리 그 값을 조정하지 않는 이상) 다음과 같은 형태가 된다고 볼 수
있습니다.
이제
*pi;
을 실행하면 2번지부터 4바이트를 가져와야 하는 상황이 발생합니다. 즉,
메모리 접근 연산 "한번"으로는 4바이트를 로드할 수 없게 되는 것입니다.
따라서 보통 다음과 같은 선택이 내려질 수 있습니다.
1. int 형의 정렬 제한 위반으므로 그와 같은 접근이 거절 된다.
2. 성능의 손해를 감수하고 첫 번째 워드 32비트를 읽어 그 중 두 바이트와
두 번째 워드 32비트를 읽어 그 중 두 바이트만을 취해 int 형의 값
4 바이트를 구성한다.
즉, 이 선택은 구현체에게 맡겨집니다. 표준 C 언어의 입장에서는 이미
변환 시점부터 undefined behavior 를 일으킨 것이기에 이 외의 다른
선택도 가능합니다. 그리고 제가 보여드린 SunOS 에서의 예는 구현체가
첫번째 경우를 선택한 경우에 해당합니다.
단지 그림에서 정렬 문제라고 표시해주신 부분이 비어있기(empty) 때문에
문제가 되는 것이 아닙니다. 즉, 정렬 제한은 메모리에 저장된 "값"의 문제가
아니라 메모리 "접근" 자체의 문제입니다.
그럼...
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
자세한 설명 감사합니다.잘못된 것을 바로잡아 준 전웅님께 감사드립니다
자세한 설명 감사합니다.
잘못된 것을 바로잡아 준 전웅님께 감사드립니다.
이 부분의 글이 잘못 전달될 수 있기에 전웅님께 양해 말씀드립니다.
궁금한것이 있어 질문드립니다.
이렇게(위 코드의 주석처럼) 이해해도 되는 건가요?
아니면, 형변환으로 int 자료형 만큼 이동해야 한다고 생각해야 하나요?
또한번 잘못 이해 했습니다. 의 궁금증은 사라졌으며 바로 위 내용을
읽었다면 무시하시기 바랍니다.
많은 분들께 폐를 끼친것 같아 죄송합니다.
[quote="lovewar"]궁금한것이 있어 질문드립니다.[co
변환 순간 정렬 제한 문제가 발생하면 그 행동은 undefined behavior
입니다. 즉, 그 어떠한 변화가 일어나도 무방합니다. 예를 들어,
(int *)(a+1) 변환 결과를 갑자기 널 포인터로 만드는 것조차 허용됩니다.
하지만, 제가 의도했던 것은 다음과 같은 상황입니다.
원래 이렇게 되어야 하는 상황인데, pi 가 가지게 될 주소(=가리키게 될
곳)이 int 형에 적절하게 정렬되어 있지 않다는 것을 알게 되어 이를 (예를
들어) 아래와 같이 다시 조정하는 것을 의미한 것입니다.
이 경우 *pi 에 접근하는 것이 정렬 제한으로 인한 문제를 일으키지 않지만
이를 다시 (short *) 를 통해 변환하는 경우 원래의 포인터 값 (a+1) 과
다른 값이 되는 결과가 발생합니다. 즉,
((short *)(int *)(a+1) != (a+1)) == 1
가 될 수도 있다는 의미였습니다.
물론, 저 역시 이와 같은 구현체를 본 적은 없습니다만, 정렬 제한 위반
시에 발생할 수 있는 전형적인 현상 중 하나도 빈번하게 언급되는
예입니다.
물론, 어떤 분들은 저렇게 포인터를 조정하는게 오히려 더 비용이 들지
않느냐고 반문할 수 있습니다만, 다음과 같은 경우를 생각해보면 꼭 그렇지
않다는 것을 알 수 있습니다.
"Gibralt 6600" (--;) 시리즈는 각 데이터형마다 서로 다른 포인터 표현을
사용하는 악명 높은 머신입니다. [*] int 형은 4의 배수 번지에만 올바르게
정렬되기에 int * 는 전체 32비트 중 2비트를 다른 용도(padding bit)로
사용합니다. 즉, 00000000 00000000 00000000 000001xx 는 xx 의 값에
무관하게 4번지를 나타내는 것입니다. 이 경우 short * 의 값을 int * 로
변환할 경우 모든 비트가 표현 변화 없이 그대로 오지만 xx 부분의 값이
무시되기 때문에 6번지를 나타내느 주소값(... 00000110)은 간단히 4번지를
나타내는 값(... 000001xx)으로 인식될 수 있습니다. 이때 두 비트의
padding 을 0 으로 설정하는 경우 이(... 00000100)를 다시 short * 로
변환하면 처음(... 00000110)과 다른 4번지가 되는 것입니다. 물론,
padding bit 가 0 이 아닌 다른 비트 패턴으로 설정될 경우 더 엉뚱한
결과가 되는 것도 가능합니다 (그래서 undefined behavior 입니다).
[*] 실제 다른 포인터형에는 동일한 표현을 사용하지만, char * 에 대해서
만큼은 독특한 포인터 표현을 사용하는 환경은 실존합니다 - 보통
word-addressed system 이 이에 해당합니다.
질문-답변 과정이 결코 폐가 되지는 않는다고 생각합니다. 그렇다면 저는
세계적으로 폐를 끼치는 사람이 되어 버립니다. --;
--
Jun, Woong (woong at gmail.com)
http://www.woong.org
댓글 달기