C / C ++에서 함수 포인터와 데이터 포인터가 호환되지 않는 이유는 무엇입니까?


130

함수 포인터를 데이터 포인터로 변환하거나 그 반대로 변환하면 대부분의 플랫폼에서 작동하지만 작동하지는 않습니다. 왜 이런 경우입니까? 둘 다 단순히 주 메모리의 주소가되어 호환되지 않아야합니까?


16
POSIX에 정의 된 표준 C에 정의되어 있지 않습니다. 차이점을 염두에 두십시오.
ephemient

나는 이것에 약간 익숙하지만 "="의 오른쪽에 캐스트를하기로되어 있지 않습니까? 문제는 당신이 void 포인터에 할당하고 있다는 것 같습니다. 그러나 매뉴얼 페이지 가이 작업을 수행하므로 누군가가 나를 교육 할 수 있기를 바랍니다. 여기 dlsym을 반환 값, 예를 캐스팅하는 사람들의 '그물에 예제를 참조하십시오 daniweb.com/forums/thread62561.html
JasonWoof

9
POSIX가 데이터 유형 : §2.12.3 포인터 유형voidvoid *void *
Jonathan Leffler

2
이것은이 웹 사이트의 ABOUT 섹션에있는 질문입니다 .. :) :) 여기서 질문을보십시오
ZooZ

1
@KeithThompson : 세상은 변하고 POSIX도 변합니다. 2012 년에 쓴 내용은 2018 년에 더 이상 적용되지 않습니다. POSIX 표준이 언어를 변경했습니다. 이제 dlsym()'응용 프로그램 사용'섹션의 끝 부분을 참고하십시오void *fptr = (int (*)(int))dlsym(handle, "my_function");
Jonathan Leffler

답변:


171

아키텍처는 코드와 데이터를 동일한 메모리에 저장할 필요가 없습니다. 하버드 아키텍처를 사용하면 코드와 데이터가 완전히 다른 메모리에 저장됩니다. 대부분의 아키텍처는 코드와 데이터가 동일한 메모리에있는 Von Neumann 아키텍처이지만 C는 가능한 경우 특정 유형의 아키텍처로만 제한하지 않습니다.


15
또한 코드와 데이터가 물리적 하드웨어의 동일한 위치에 저장되어 있어도 소프트웨어 및 메모리 액세스는 종종 운영 체제 "승인"없이 코드로 데이터를 실행하지 못하게합니다. DEP 등.
Michael Graczyk

15
적어도 다른 주소 공간을 갖는 것보다 더 중요한 것은 함수 포인터가 데이터 포인터와 다른 표현을 가질 수 있다는 것입니다.
Michael Burr

14
다른 주소 공간을 사용하여 코드와 데이터 포인터를 갖기 위해 Harvard 아키텍처가 없어도됩니다. 이전 DOS "Small"메모리 모델에서이 작업을 수행했습니다 (포인터 근처 CS != DS).
caf

1
최신 프로세서조차도 운영 체제에서 코드를 작성할 수있는 경우에도 명령 및 데이터 캐시가 일반적으로 개별적으로 처리되는 것과 같은 혼합으로 어려움을 겪을 것입니다.
PypeBros

3
@EricJ. 호출 할 때까지 VirtualProtect데이터 영역을 실행 가능으로 표시 할 수 있습니다.
Dietrich Epp

37

일부 컴퓨터에는 코드와 데이터를위한 별도의 주소 공간이 있습니다. 이러한 하드웨어에서는 작동하지 않습니다.

이 언어는 현재 데스크톱 응용 프로그램뿐만 아니라 다양한 하드웨어에서 구현 될 수 있도록 설계되었습니다.


C 언어위원회는 결코 void*함수에 대한 포인터가 될 의도 가 없었던 것 같습니다. 그들은 객체에 대한 일반적인 포인터를 원했습니다.

C99의 이론적 근거는 다음과 같습니다.

6.3.2.3 포인터
C는 이제 광범위한 아키텍처에서 구현되었습니다. 이러한 아키텍처 중 일부는 일부 정수 유형의 크기 인 균일 포인터를 특징으로하지만 최대 이식 가능한 코드는 다른 포인터 유형과 정수 유형 사이에 필요한 대응 관계를 가정 할 수 없습니다. 일부 구현에서 포인터는 정수 유형보다 넓을 수도 있습니다.

일반적인 객체 포인터 유형 으로 void*(“포인터 void”)를 사용하는 것은 C89위원회의 발명입니다. 이 유형의 채택은 임의의 포인터를 조용히 변환 fread하거나 (에서와 같이 ) 인수 유형이 정확하게 일치하지 않으면 (에서와 같이 strcmp) 함수 프로토 타입 인수를 지정하려는 바람에 자극을 받았습니다 . 객체 포인터 및 / 또는 정수와 맞지 않는 함수에 대한 포인터에 대해서는 언급되지 않았습니다.

참고 마지막 단락에서 함수대한 포인터에 대해서는 언급되지 않았습니다. 그것들은 다른 지침과 다를 수 있으며위원회는이를 알고 있습니다.


표준은 데이터 유형을 동일한 크기로 만들고 한 번에 할당 한 다음 다시 할당하면 동일한 값을 보장함으로써 이것을 망칠 필요없이 호환 가능하게 만들 수 있습니다. void *를 사용하여이 작업을 수행합니다. void *는 모든 유형과 호환되는 유일한 포인터 유형입니다.
Edward Strange

15
@CrazyEddie에 함수 포인터를 할당 할 수 없습니다 void *.
ouah

4
void * 수용 함수 포인터에서 잘못 될 수 있지만 요점은 남아 있습니다. 비트는 비트입니다. 표준은 서로 다른 유형의 크기가 서로 데이터를 수용 할 수 있어야하며 다른 메모리 세그먼트에서 사용하더라도 할당이 작동하도록 보장 할 수 있습니다. 이 비 호환성이 존재하는 이유는 이것이 표준에 의해 보장되지 않기 때문에 과제에서 데이터가 손실 될 수 있기 때문입니다.
Edward Strange

5
그러나 sizeof(void*) == sizeof( void(*)() )함수 포인터와 데이터 포인터의 크기가 다른 경우 공간이 낭비됩니다. 이것은 최초의 C 표준이 작성된 80 년대의 일반적인 사례입니다.
Robᵩ

8
@RichardChambers : 다른 주소 공간의 주소 너비 도 다를 수 있습니다 ( 예 : 명령어에는 16 비트, 데이터에는 8 비트를 사용 하는 Atmel AVR ; 이 경우 데이터 (8 비트)에서 함수 (16 비트) 포인터로 다시 변환하기가 어렵습니다. C는 구현하기 쉬워야한다. 데이터와 명령어 포인터가 서로 호환되지 않는 상태로 두는 것이 쉬워집니다.
John Bode

30

MS-DOS, Windows 3.1 이상을 기억하는 사람들은 대답이 매우 쉽습니다. 이들 모두는 코드 및 데이터 포인터에 대한 다양한 특성 조합으로 여러 가지 메모리 모델을 지원하는 데 사용되었습니다.

예를 들어 Compact 모델 (작은 코드, 큰 데이터)의 경우 :

sizeof(void *) > sizeof(void(*)())

반대로 중간 모델 (큰 코드, 작은 데이터)에서 :

sizeof(void *) < sizeof(void(*)())

이 경우 코드와 날짜를위한 별도의 저장소가 없지만 두 개의 포인터 사이에서 변환 할 수 없습니다 (비표준 __near 및 __far 수정자를 사용하지 않는 경우).

또한 포인터의 크기가 동일하더라도 포인터가 동일한 것을 가리키는 것은 아닙니다. DOS 스몰 메모리 모델에서는 포인터와 함께 사용되는 코드와 데이터가 서로 다르지만 서로 다른 세그먼트를 가리 켰습니다. 따라서 함수 포인터를 데이터 포인터로 변환해도 함수와 전혀 관련이없는 포인터는 제공되지 않으므로 그러한 변환에는 사용되지 않았습니다.


Re : "함수 포인터를 데이터 포인터로 변환한다고해서 함수와 전혀 관련이없는 포인터를 얻을 수 없으므로 그러한 변환에 사용되지 않았습니다.": 이것은 완전히 따르지 않습니다. 변환하기 int*A를 void*당신이 정말로 아무것도 할 수 없다는 포인터주고,하지만 여전히 유용한 것은 변환을 수행 할 수 있습니다. (이것은 모든 객체 포인터를 void*저장할 수 있기 때문에 보유하고있는 유형을 알 필요가없는 일반 알고리즘에 사용될 수 있습니다. 동일한 경우 함수 포인터에도 유용 할 수 있습니다.
ruakh

4
@ruakh : 변환의 경우 int *void *는이 void *원래와 같은 객체에 적어도 점을 보장 int *했다 - 이것은 일반적인 알고리즘에 유용 그래서 액세스 뾰족한-에 객체처럼 int n; memcpy(&n, src, sizeof n);. 함수 포인터를로 변환 void *해도 함수를 가리키는 포인터가 생성되지 않으면 그러한 알고리즘에는 유용하지 않습니다. void *다시 할 수있는 유일한 것은 다시 함수 포인터로 다시 변환하는 것입니다. 잘 union포함 된 void *및 함수 포인터를 사용하십시오.
caf

@caf : 충분합니다. 지적 해 주셔서 감사합니다. 그리고 그 문제를 들어,이 경우에도 void* 함수에 점을, 나는 그것이 사람에 전달하기위한 좋은 생각이 될 것입니다 생각 memcpy. - P
ruakh

위에서 복사 : POSIX가 데이터 유형 : §2.12.3 포인터 유형voidvoid *void *
Jonathan Leffler

@caf 올바른 유형 을 알고 있는 콜백으로 전달 해야하는 경우 왕복 안전에만 관심이 있습니다. 변환 된 값과 다른 관계는 없습니다.
중복 제거기

23

void에 대한 포인터는 모든 종류의 데이터에 대한 포인터를 수용 할 수 있어야하지만 반드시 함수에 대한 포인터는 아닙니다. 일부 시스템은 데이터에 대한 포인터와 기능에 대한 포인터에 대한 요구 사항이 다릅니다 (예 : 데이터 대 코드에 대한 주소 지정이 다른 DSP가 있으며 MS-DOS의 중간 모델은 코드에 32 비트 포인터를 사용하지만 데이터에는 16 비트 포인터 만 사용함) .


1
그러나 dlsym () 함수가 void * 이외의 것을 반환해서는 안됩니다. 만약 void *가 함수 포인터를 위해 충분히 크지 않다면, 우리는 이미 혼란스러워하지 않습니까?
Manav

1
@ Knickerkicker : 그렇습니다. 메모리가 제공되는 경우 dlsym의 리턴 유형은 아마도 9-10 년 전에 OpenGroup의 이메일 목록에서 길게 논의되었습니다. 그럼에도 불구하고 나는 (어떤 것이라도) 그 결과가 무엇인지 기억하지 못한다.
Jerry Coffin

1
네가 옳아. 이것은 당신의 요점에 대한 상당히 좋은 (구식이기는하지만) 요약으로 보입니다.
Manav


2
@LegoStormtroopr : 21 사람들이 동의하는 방법을 재미있는 아이디어 까지 - 투표, 만 3 실제로 그렇게했다. :-)
Jerry Coffin

13

이미 여기에 말한 것 외에도 POSIX를 보는 것이 흥미 롭습니다 dlsym().

ISO C 표준에서는 함수에 대한 포인터를 데이터에 대한 포인터로 앞뒤로 캐스트 할 필요가 없습니다. 실제로, ISO C 표준은 void * 유형의 객체가 함수에 대한 포인터를 보유 할 것을 요구하지 않습니다. 그러나 XSI 확장을 지원하는 구현에서는 void * 유형의 오브젝트가 함수에 대한 포인터를 보유 할 수 있어야합니다. 그러나 함수에 대한 포인터를 다른 데이터 유형에 대한 포인터로 변환 한 결과 (void * 제외)는 여전히 정의되지 않았습니다. void * 포인터에서 함수 포인터로의 변환을 시도하는 경우 ISO C 표준을 준수하는 컴파일러에서 경고를 생성해야합니다.

 fptr = (int (*)(int))dlsym(handle, "my_function");

여기에 언급 된 문제점으로 인해, 향후 버전에서는 함수 포인터를 리턴하기 위해 새 함수를 추가하거나 현재 인터페이스는 두 가지 새로운 함수 (데이터 포인터를 리턴하는 함수와 함수 포인터를 리턴하는 함수)를 위해 더 이상 사용되지 않을 수 있습니다.


dlsym을 사용하여 함수의 주소를 얻는 것이 현재 안전하지 않다는 것을 의미합니까? 현재 안전한 방법이 있습니까?
gexicide

4
즉, 현재 POSIX는 플랫폼 ABI에서 기능 및 데이터 포인터를 안전하게 전송 및 전송할 수 있어야 void*합니다.
Maxim Egorushkin

@gexicide POSIX를 준수하는 구현이 언어를 확장하여 표준 자체에 따라 정의되지 않은 동작에 대한 구현 정의 의미를 부여 함을 의미합니다. C99 표준에 대한 일반적인 확장 중 하나 인 J.5.7 함수 포인터 캐스트 섹션에도 나와 있습니다.
David Hammen

1
@DavidHammen 언어에 대한 확장이 아니라 새로운 추가 요구 사항입니다. void*POSIX와 달리 C는 함수 포인터와 호환 되지 않아도 됩니다.
Maxim Egorushkin

9

C ++ 11은 C / C ++와 POSIX의 오랜 불일치에 대한 솔루션을 제공합니다 dlsym(). reinterpret_cast구현이이 기능을 지원하는 한 함수 포인터를 데이터 포인터로 /에서 변환하는 데 사용할 수 있습니다 .

표준에서 5.2.10 para. 8, "함수 포인터를 객체 포인터 유형으로 또는 그 반대로 변환하는 것은 조건부로 지원됩니다." 1.3.5 "조건부 지원"은 "구현이 필요하지 않은 프로그램 구성"으로 정의


하나는 할 수 있지만, 그렇지 않아야합니다. 적합한 컴파일러 는 이에 대한 경고를 생성 해야합니다 (이는 오류를 유발해야합니다, 참조 -Werror). 더 나은 (그리고 비 UB가 아닌) 해결책은 (예를 들어 )에 의해 반환 된 객체에 대한 포인터 를 검색하고 함수 포인터 에 대한 포인터 로 변환하는 것 입니다. 여전히 구현 정의되었지만 더 이상 경고 / 오류가 발생하지 않습니다 . dlsymvoid**
Konrad Rudolph

3
@ KonradRudolph : 동의하지 않습니다. "조건부 지원"문구는 특별히 경고없이 허용 dlsym하고 GetProcAddress컴파일 하기 위해 작성되었습니다 .
MSalters

@MSalters "동의하다"는 무슨 뜻입니까? 내가 옳고 그른지 dlsym을 문서는 명시 적으로 말했다 가 "ISO C 표준에 부합하는 컴파일러는 함수 포인터에 무효 * 포인터에서 변환을 시도하면 경고를 생성해야합니다." 이것은 추측의 여지가 많지 않습니다. 그리고 (와 GCC는 -pedantic) 않습니다 경고한다. 다시 말하지만, 추측은 불가능합니다.
Konrad Rudolph

1
후속 조치 : 이제 이해합니다. UB가 아닙니다. 구현 정의입니다. 경고를 생성해야하는지 여부는 여전히 확실하지 않습니다. 오 잘
Konrad Rudolph

2
@ KonradRudolph : 나는 당신의 "하지 말아야"하는 의견에 동의하지 않았습니다. 대답은 구체적으로 C ++ 11을 언급했으며 문제가 해결 될 당시 C ++ CWG의 회원이었습니다. C99는 실제로 다른 표현을 가지고 있으며 조건부 지원 C ++ 발명입니다.
MSalters

7

대상 아키텍처에 따라 코드와 데이터는 근본적으로 호환되지 않고 물리적으로 별 개인 메모리 영역에 저장 될 수 있습니다.


'물리적으로 구별되는'이해하지만 '기본적으로 호환되지 않는'구별에 대해 더 자세히 설명 할 수 있습니다. 내가 질문에서 말했듯이 무효 포인터는 포인터 유형만큼 크지 않아야합니다. 또는 내 입장에서 잘못된 추정입니다.
Manav

@KnickerKicker : void *데이터 포인터를 보유 할만큼 크지 만 반드시 함수 포인터는 아닙니다.
ephemient

1
다시 미래로 : P
SSpoke

5

undefined가 반드시 허용되지 않는다는 의미는 아니며, 컴파일러 구현자가 원하는 방식으로 더 자유롭게 할 수 있음을 의미 할 수 있습니다.

예를 들어 일부 아키텍처에서는 불가능할 수도 있습니다. 정의되지 않은 경우이를 수행 할 수없는 경우에도 준수하는 'C'라이브러리를 가질 수 있습니다.


5

다른 해결책 :

POSIX는 동일한 크기와 표현하도록 기능과 데이터 포인터를 보장 가정 (나는이 텍스트를 찾을 수 있지만, 예를 들어, 영업 이익은 인용하는 것은 적어도 그들이 제안 목적 , 다음 작동합니다 이러한 요구 사항을 만들기 위해) :

double (*cosine)(double);
void *tmp;
handle = dlopen("libm.so", RTLD_LAZY);
tmp = dlsym(handle, "cos");
memcpy(&cosine, &tmp, sizeof cosine);

이렇게하면 char []모든 유형의 별명을 지정할 수 있는 표시를 통해 별명 지정 규칙을 위반하지 않습니다.

또 다른 접근법 :

union {
    double (*fptr)(double);
    void *dptr;
} u;
u.dptr = dlsym(handle, "cos");
cosine = u.fptr;

그러나 memcpy절대적으로 100 % 정확한 C를 원한다면 접근법 을 권장합니다 .


5

공간 요구 사항이 다른 여러 유형이 될 수 있습니다. 하나에 할당하면 포인터 값을 돌이킬 수 없게 슬라이스하여 다시 할당하면 다른 결과가 나올 수 있습니다.

표준이 필요하지 않을 때 또는 크기로 인해 CPU가 그것을 사용하기 위해 여분의 쓰레기를 만들어야 할 때 공간을 절약 할 수있는 구현을 제한하고 싶지 않기 때문에 유형이 다를 수 있다고 생각합니다.


3

실제로 휴대 가능한 유일한 솔루션은 dlsym함수 에 사용하지 않고 dlsym함수 포인터가 포함 된 데이터에 대한 포인터를 얻는 데 사용 하는 것입니다. 예를 들어, 라이브러리에서 :

struct module foo_module = {
    .create = create_func,
    .destroy = destroy_func,
    .write = write_func,
    /* ... */
};

그런 다음 응용 프로그램에서 :

struct module *foo = dlsym(handle, "foo_module");
foo->create(/*...*/);
/* ... */

또한, 이는 어쨌든 좋은 설계 방식이며 dlopen동적 연결을 지원하지 않는 시스템 또는 사용자 / 시스템 통합자가 동적 연결을 사용하지 않으려는 시스템의 모든 모듈을 통한 동적 로딩 및 정적 연결을 모두 쉽게 지원할 수 있습니다.


2
좋은! 나는 이것이 더 유지하기 쉽다고 동의하지만, 이것 위에 정적 연결을 망치는 방법은 여전히 ​​명확하지 않습니다. 정교하게 할 수 있습니까?
Manav

2
각 모듈이 foo_module고유 한 이름을 가진 자체 구조를 가지고 있다면 , 단순히 struct { const char *module_name; const struct module *module_funcs; }"로드"할 모듈을 찾기 위해이 테이블을 검색하고 올바른 포인터를 반환하는 간단한 함수와 배열로 추가 파일을 만들 수 있습니다. 대신에 dlopendlsym.
R .. GitHub 중지 지원 얼음

@R .. 사실이지만 모듈 구조를 유지해야하므로 유지 관리 비용이 추가됩니다.
user877329

3

함수 포인터의 크기가 데이터 포인터와 크기가 다른 현대적인 예 : C ++ 클래스 멤버 함수 포인터

https://blogs.msdn.microsoft.com/oldnewthing/20040209-00/?p=40713/ 에서 직접 인용

class Base1 { int b1; void Base1Method(); };
class Base2 { int b2; void Base2Method(); };
class Derived : public Base1, Base2 { int d; void DerivedMethod(); };

이제 두 가지 가능한 this포인터가 있습니다.

의 멤버 함수에 대한 포인터는의 멤버 함수에 Base1대한 포인터로 사용될 수 있습니다 . Derived둘 다 동일한 this포인터를 사용하기 때문 입니다. 그러나 멤버 함수에 Base2대한 포인터는 포인터를 조정해야하기 Derived때문에의 멤버 함수에 대한 포인터로 그대로 사용할 수 없습니다 this.

이를 해결하는 방법에는 여러 가지가 있습니다. Visual Studio 컴파일러가이를 처리하기로 결정한 방법은 다음과 같습니다.

다중 상속 클래스의 멤버 함수에 대한 포인터는 실제로 구조입니다.

[Address of function]
[Adjustor]

다중 상속을 사용하는 클래스의 포인터 대 멤버 함수의 크기는 포인터의 크기에 a의 크기를 더한 것입니다 size_t.

tl; dr : 다중 상속을 사용할 때 (컴파일러, 버전, 아키텍처 등에 따라) 멤버 함수에 대한 포인터는 실제로 다음과 같이 저장 될 수 있습니다

struct { 
    void * func;
    size_t offset;
}

어느 것보다 분명히 큽니다 void *.


2

대부분의 아키텍처에서 모든 일반 데이터 유형에 대한 포인터는 동일한 표현을 가지므로 데이터 포인터 유형간에 캐스트하는 것은 아무 문제가 없습니다.

그러나 함수 포인터는 다른 포인터를 필요로 할 수 있으며 아마도 다른 포인터보다 클 수 있습니다. void *가 함수 포인터를 보유 할 수 있으면 void *의 표현이 더 커야 함을 의미합니다. 그리고 void *로 /로부터의 모든 데이터 포인터 캐스트는이 추가 복사를 수행해야합니다.

누군가 언급했듯이, 필요한 경우 노동 조합을 사용하여 달성 할 수 있습니다. 그러나 대부분의 void * 사용은 데이터 용이므로 함수 포인터를 저장해야하는 경우를 대비하여 모든 메모리 사용을 늘리는 것이 번거 롭습니다.


-1

나는이 2012 년 이후에 댓글되지 않았 음을 알고 있지만, 나는 내가 있음을 추가하는 것이 유용 할 것이라고 생각 할 수 있는 아키텍처 알고 매우 그 아키텍처 검사 권한에 호출하기 때문에 데이터와 기능에 대한 호환되지 않는 포인터 및 추가 정보를 전달합니다. 캐스팅이 도움이되지 않습니다. 그것은이다 .


이 답변은 잘못되었습니다. 예를 들어 함수 포인터를 데이터 포인터로 변환하고 읽을 수 있습니다 (평소와 같이 해당 주소에서 읽을 수있는 권한이있는 경우). 결과는 예를 들어 x86에서와 마찬가지로 의미가 있습니다.
Manuel Jacob
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.