ELF 기호: 전역+숨김

ELF 기호: 전역+숨김

예를 들어이 Oracle 설명:

STB_GLOBAL 전역 기호입니다.이 기호는 모든 개체 파일에 표시됩니다.병합됩니다. 한 파일의 전역 기호 정의는 동일한 전역 기호에 대한 다른 파일의 정의되지 않은 참조를 충족합니다. ...

STV_HIDDEN 이름이 숨겨진 경우 현재 구성 요소에 정의된 기호다른 구성 요소에 보이지 않음. 이러한 기호는 보호되어야 합니다. 이 속성은 구성 요소의 외부 인터페이스를 제어하는 ​​데 사용됩니다. 이러한 기호로 명명된 개체는 해당 주소가 외부로 전달되는 경우 다른 구성 요소에서 계속 참조될 수 있습니다.
재배치 가능한 객체에 포함된 숨겨진 기호는 제거되거나 STB_LOCAL 바인딩으로 변환됩니다.재배치 가능한 객체 파일이 실행 파일이나 공유 객체 파일에 포함될 때 링크 편집기에 의해 실행됩니다.

그러나 몇 가지 간단한 테스트 프로그램(x86-64 Linux에서 GCC로 컴파일됨)을 살펴보면 readelf -s몇 가지 전역 숨겨진 기호가 있습니다.

FUNC    GLOBAL HIDDEN    16 _fini
OBJECT  GLOBAL HIDDEN    25 __dso_handle
OBJECT  GLOBAL HIDDEN    25 __TMC_END__

위의 설명에 따르면 이는 터무니없는 일이며 단순히 허용되지 않습니다.

이 조합에는 어떤 속성(가시성, 삽입 기능...)이 있습니까?

답변1

이제 좀 이해가 되네요.

GLOBAL+HIDDEN은 일반적으로 .o함수를 숨기는 링크되지 않은 파일(정적 라이브러리 포함)에서 발생합니다(반면 유닛-로컬 정적 함수는 이미 LOCAL+DEFAULT임). 그런 다음 연결하는 동안 GLOBAL+HIDDEN은 LOCAL+DEFAULT로 변환되며 이는 항상 일반 기능에 사용됩니다.

이는 공유 라이브러리를 생성할 때 문제의 GCC/lib 내부에서도 발생합니다. 그러나 라이브러리가 아닌 실행 파일의 경우 이러한 내부는 순전히 편의상의 이유로 완전히 변환되지 않고 전역+숨겨진 상태로 유지됩니다. 어디에도 링크된 라이브러리가 아니기 때문에 아무런 해를 끼치지 않습니다.

어쩌면 요약될 수도 있겠네요일반 사용자 정의 함수다른 사람들에게도 도움이 됩니다... 이것은 Linux 동작에 관한 것입니다. 다른 시스템에서는 약간 다를 수 있습니다.

고려해야 할 사항(정상 기능)

바이너리를 만들어 봅시다 mybin. 여기 에는 세 개의 파일이 있으며 fila.c코드 파일 중 하나에 함수가 있습니다 .filb.cfilc.cfunc1

mybin정적 라이브러리(컴파일 .o및 보관된 경우에만), 실행 가능한 프로그램(링크된 경우도 있음) 또는 공유 라이브러리(링크된 경우인 경우)가 될 수 있습니다.

func1코드는 다음과 같습니다.

  • 정상적인 기능
  • 약한 ( __attribute__((weak)))
  • 숨김, mybin공유 라이브러리를 사용하는 프로그램에서는 사용할 수 있지만 공유 라이브러리를 사용하는 프로그램에서는 사용할 수 없음(있는 경우)을 __attribute__ ((__visibility__ ("hidden")))의미 합니다.
  • 정적, .c정의된 파일에서만 사용 가능( staticC에서는)
  • (보호 및 내부도 마찬가지이지만 실제로는 중요하지 않습니다)

이는 기호를 완전히 제거할 수 있는 최적화(컴파일 시간 및/또는 LTO(링크 시간 최적화))를 고려하지 않습니다.

컴파일 단계(~ .o)

fila.c포함된 파일 에서 func1함수는 모든 유형(일반/약함/숨김/정적)에 대해 분명히 표시되고 사용 가능합니다.

  • 정상적인 기능:
    • filb.cfilc.c다른 유닛 (및 정적 라이브러리(있는 경우)) 에도 표시됩니다 .
    • 모든 유닛에는 해당 이름을 가진 일반/강력 함수가 하나만 있을 수 있습니다. 그렇지 않으면 링커가 나중에 불평할 것입니다(이것은 실행 가능/공유 라이브러리/정적 라이브러리)(링커 옵션과 같은 이상한 항목은 제외 muldefs).
    • 생성된 파일에서 .o함수는 GLOBAL+DEFAULT입니다.
  • 덜 강력함
    • 다른 유닛에도 표시됨
    • 하나의 일반/강한 기능 외에도 이를 둘러싼 여러 개의 약한 기능이 있을 수 있습니다. 이 경우, 나중에 접속 시 모든 유닛에 강한 것이 사용되고 약한 것은 사라집니다.
    • 약한 함수만 있고 해당 이름을 가진 강한 함수가 없는 경우 첫 번째 약한 함수가 승리합니다(즉, 그러한 약한 함수가 있는 컴파일러 명령줄의 첫 번째 파일).
    • 생성된 파일에서 .o함수는 WEAK+DEFAULT입니다.
  • 숨겨진 기능
    • GLOBAL+HIDDEN이 됩니다(파일에서만 .o)
    • 그렇지 않으면 일반 함수처럼 동작합니다.
  • 약함 + 숨겨진 기능
    • 약해지다+숨겨지다
    • 그렇지 않으면 약한 기능처럼 동작합니다.
  • 정적 함수
    • 로컬+기본값으로 전환
    • 다른 유닛에는 표시되지 않습니다. 이를 사용하면 유닛 전체에서 사용될 때 모든 LOCAL이 무시되기 때문에 링커가 나중에 불평하게 됩니다.
    • 여러 유닛이 해당 이름을 가진 고유한 정적 기능을 가질 수 있으며 모두 다를 수 있습니다.
  • .o파일에 LOCAL+HIDDEN이 없습니다.

컴파일 시간 연결

정적 라이브러리를 생성하면 연결이 발생하지 않습니다.

공유 라이브러리 및 실행 파일의 경우:

  • 모든 로컬(즉, 정적 함수)은 완전히 무시되고, 다른 유닛에 표시되지 않으며, 사용 가능한 공유 라이브러리 함수로 내보내지지 않습니다.
  • 위에서 언급한 것처럼 GLOBAL/WEAK+DEFAULT를 사용할 수 있으며, 공유 라이브러리의 경우 사용 가능한 함수 목록에 입력됩니다() 같은 종류가 있습니다.
  • GLOBAL/WEAK+HIDDEN은 위와 같은 바이너리 단위로 사용할 수 있지만, 링킹 중에는 타입이 LOCAL+DEFAULT로 변경됩니다(링크 전의 정적 함수와 같습니다). 공유 라이브러리 및 사용 가능한 기능 목록(), 이는 전혀 입력되지 않거나 런타임 시 무시된다는 의미입니다.

호출되는 함수 이름은 방금 생성된 바이너리나 링커 명령줄에 지정된 공유 라이브러리(및 GLOBAL/WEAK+DEFAULT)에 일반적으로 존재해야 합니다.
앞에서 언급했듯이 완전히 링크된 바이너리(공유 라이브러리 또는 실행 파일)에는 동일한 이름을 가진 여러 GLOBAL/WEAK+DEFAULT 함수가 없으며 최대 1개만 포함됩니다. 바이너리 파일당 하나입니다. 그러나 서로 다른 바이너리 간에는 여전히 중복이 있을 수 있습니다. 즉, 두 공유 라이브러리(또는 실행 파일과 공유 라이브러리 모두)에 이 가 있을 수 있습니다 func1. 이는 아래 삽입 섹션에 설명된 대로 조회를 유발하지 않습니다.

연결하는 동안 사용된 함수 이름이 어디에도 구현되지 않은 경우(실행 파일이나 사용된 공유 라이브러리 모두) 일반적으로 오류가 발생합니다. 그러나 정의( .h파일 등에서 코드 없음)가 WEAK로 표시된 경우 해당 링크는 오류가 발생하지 않으며 런타임 시 정의를 찾을 수 없으면 널 포인터로 평가됩니다( check 를 사용할 수 있음 if). . 런타임 시 공유 라이브러리가 동시에 변경되었거나 LD_PRELOAD더 많은 라이브러리가 추가되었기 때문임을 알 수 있습니다.

런타임 개입 및 최적화

함수 호출은 이진 명령어를 생성할 수 있습니다.

  • 플러그 가능하지 않음: 이러한 호출은 동일한 바이너리(동일한 공유 라이브러리 또는 동일한 실행 파일, 다른 유닛 또는 동일한 유닛에 있을 수도 있음.o ) 주소에 존재하는 일부 미리 계산된(절대/상대) 함수로 점프합니다. 이는 또한 함수 인라인과 같은 최적화를 가능하게 합니다.
  • interposable: 런타임 시 런타임 링커는 ld.so해당 이름을 가진 함수가 어디에 있는지 묻고 다른 프로그램 실행에서 다른 결과를 생성할 수 있습니다. 이는 또한 함수 인라인이 없음을 의미하며 성능이 약간 저하될 수 있습니다. 그러나 아래를 참조하여 함수를 재정의할 수 있습니다.

바이너리(실행 파일 → 공유 라이브러리 또는 공유 라이브러리 1 → 공유 라이브러리 2) 간의 호출은 항상 연결 가능합니다.
동일한 바이너리에 있는 두 개의 "자체" 함수 간의 호출:

  • 로컬 기능(정적, 숨김)은 플러그할 수 없습니다.
  • GLOBAL/WEAK 함수의 경우 이 바이너리를 생성할 때 컴파일러/링커 옵션에 따라 달라집니다(예: fPIC fPIE fno-semantic-interposition -Bsymbolic -rdynamic 등).

실행 중인 프로그램 내 호출에 ld.so검색이 필요한 경우 func1:

  • 항상 프로그램의 실행 가능한 바이너리를 먼저 확인하십시오.
  • 기본 exe에서 찾을 수 없는 경우 LD_PRELOAD공유 라이브러리(있는 경우)가 고려됩니다.
  • 그런 다음 프로그램에서 사용하는 모든 공유 라이브러리는 연결 중에 지정된 순서(명령줄의 순서)에 따라 검사됩니다.
    GLOBAL/WEAK+DEFAULT 기능만 고려됩니다. 이 단계에서는 GLOBAL과 WEAK 사이에 차이가 없습니다. 이전 검사 위치의 WEAK는 이후 라이브러리의 약하지 않은 기능보다 우선합니다. (역사적 차이점은 오래 전에 존재했지만 Linux에서는 오래 전에 사라졌습니다.)

이는 예를 들어 다음을 의미합니다.

  • 바이너리 간의 함수 호출은 항상 LD_PRELOAD라이브러리에 의해 재정의될 수 있습니다. 기본 실행 파일이 func1일부 공유 라이브러리에서 호출하려고 하고 사전 로드된 라이브러리에 다른 라이브러리가 있는 경우 func1사전 로드된 라이브러리가 승리합니다. 공유 라이브러리 간의 호출은 유사하며 미리 로드된 라이브러리가 항상 먼저 검색됩니다.
  • 공유 라이브러리가 아닌 기본 실행 파일에 존재하는 함수는 에서도 덮어쓸 수 없습니다 . 따라서 LD_PRELOAD프로그램을 컴파일하는 동안 프로그램 내의 함수에 대한 호출이 컴파일되는지 여부는 실제로 중요하지 않습니다. ld.so그 자체가 항상 승리합니다. (컴파일 모드는 공유 라이브러리에 중요합니다.)
  • sharedlib2 → sharedlib3 사이의 호출은 이전 sharedlib1 또는 기본 실행 파일에 의해 덮어쓰기될 수 있으므로 sharedlib2는 sharedlib3 대신 이를 호출합니다.
  • 컴파일 방법에 따라 sharedlib2의 전역 함수에 대한 호출은 기본 실행 파일 및 이전 라이브러리에 의해 재정의될 수 있습니다. 또는 더 최적화될 수 있지만 재정의가 인식되지 않습니다.
  • 컴파일러 플래그 없이 라이브러리의 특정 함수를 덮어쓰는 것을 방지하여 더 나은 최적화와 더 빠른 연결을 가능하게 하려면 해당 함수를 코드에서 숨김(또는 정적으로)으로 선언할 수 있습니다. 그러나 이는 또한 해당 함수를 기본 실행 파일에서 사용할 수 없음을 의미합니다(특히 재정의되지 않은 경우). 함수별 컴파일러 옵션 없이 작동하는 중간 접근 방식은 함수를 숨김으로 선언하고 전역 별칭을 추가하는 것입니다. 내부 라이브러리는 재정의되지 않은 최적화된 숨겨진 함수를 호출하고, 외부의 모든 것은 별칭을 사용합니다(다른 라이브러리에 의해 재정의될 수 있음 등).

실행 파일에 관한 참고 사항

다른 바이너리에서 재정의 가능한 호출을 재정의해야 하는 모든 함수는자체 바이너리의 일부입니다. 모든 GLOBAL/WEAK 함수는 공유 라이브러리에 자동으로 제공되지만 기본 실행 파일에는 반드시 그런 것은 아닙니다.

readelf -s목록의 내용을 표시할 수 있습니다. GCC를 사용한 테스트에서 링커는 컴파일 타임에 일부 공유 라이브러리에도 해당 이름의 함수가 있음을 발견하면(코드 내용에 관계없이) 함수를 추가하는 것으로 보이지만 함수가 고유한 것으로 보이는 경우에는 그렇지 않습니다. 추가될 것입니다(그러면 아무것도 재정의되지 않기 때문입니다).

어떤 경우에는 문제가 될 수 있습니다. 예를 들어, 컴파일하는 동안 기본 실행 파일에만 하나가 있지만 func1나중에 사용된 공유 라이브러리 중 하나가 func1하나를 갖도록 변경되면(기본 프로그램을 다시 컴파일하지 않고) 기본 프로그램은 func1라이브러리 중 하나를 덮어쓸 수 없습니다.

이를 방지하려면 항상 모든 함수를 나열하고 사용하십시오 -rdynamic(주 실행 파일에서만, 라이브러리에서는 쓸모가 없습니다).

마지막으로 방금 언급한 PROTECTED 모드의 문제점은 무엇입니까?

원칙적으로 라이브러리 함수에 대한 GLOBAL+PROTECTED는 위에서 언급한 숨겨진 함수 별칭처럼 작동해야 합니다. 라이브러리에는 라이브러리 자체 내에서 사용되고 최적화되는 숨겨진(LOCAL+DEFAULT) 함수가 있지만 이를 재정의할 수는 없습니다. 또한 외부에서 사용(및 재정의)할 수 있는 공개 별칭도 있습니다.

PROTECTED를 사용하면 HIDDEN을 설정하고 별칭을 만들 필요가 없습니다.

그러나 두 가지 문제가 있습니다.

  • ld.soPROTECTED는 세부 사항 및 C ABI 요구 사항(동일한 기능의 동일한 주소) 으로 인해 HIDDEN+alias보다 느립니다.
  • GCC 19520과 같은 버그가 있습니다.

관련 정보