c++ string 포인터
글쓴이: awdxawdx101 / 작성시간: 일, 2020/07/05 - 6:54오후
// g++ -o string1 string1.cpp -mpreferred-stack-boundary=4 -no-pie -fno-pic -fno-stack-protector #include <iostream> #include <stdio.h> using namespace std; int main(void) { string str = "aaaaaa"; string *ptr = &str; printf("%p\n", *ptr); printf("%p\n", *(ptr+(int)0x10)); return 0; }
위 str이 그냥 "aaaaa"의 포인터만 저장하는줄 알았는데 그게 아니였습니다.
문자열 길이에 따라 다르지만, 위 경우는 아래처럼 구성이 되었습니다.
=== STACK ===
주소 / 값
[20] = 30 <--- str
[28] = len("aaaa")
[30] = "aaaa"
그리고 ~basic_string() 에서는 문자열이 스택에 할당된 경우
[20] + 10 과 [20]이가지고 있는 값(30)을 비교합니다.
같으면 그냥 리턴해주는데 다르면 delete()를 해주더라구요.(다르면 heap에 저장되는 경우)
=> 0x43c480 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()>: mov rax,rdi 0x43c483 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()+3>: mov rdi,QWORD PTR [rdi] 0x43c486 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()+6>: add rax,0x10 0x43c48a <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()+10>: cmp rdi,rax 0x43c48d <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()+13>: je 0x43c498 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()+24> 0x43c48f <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()+15>: jmp 0x402390 <operator delete(void*)> 0x43c494 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()+20>: nop DWORD PTR [rax+0x0] 0x43c498 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()+24>: repz ret
그래서 포인터 변수를 이용해 [20]에 들어있는 30을 다른값으로 바꾸면 어떻게 되는지 궁금해서 맨 위의 코드를 만들었습니다.
일단 위 코드에서
*(ptr+0x10)
을 했는데 여기서 계속 0x200을 더합니다. 혹시 타입때문인가해서 (int)까지 해주었는데도 0x200을 더하는건 변함이 없습니다.
0x0000000000400e12 <+84>: mov rax,QWORD PTR [rbp-0x18] 0x0000000000400e16 <+88>: add rax,0x200 <<<<<<<<<<<<<<<<<< 0x0000000000400e1c <+94>: mov rsi,rax 0x0000000000400e1f <+97>: mov edi,0x54e78b 0x0000000000400e24 <+102>: mov eax,0x0 0x0000000000400e29 <+107>: call 0x4b5e90 <printf>
포인터 변수를 이용해 string 타입의 데이터를 건드리지는 못하는건가요?
아니면 제가 놓치고있는 부분이 있는건가요?
Forums:
포인터에 0X10 을 더하면 실제로 더해지는 수는
포인터에 0X10 을 더하면 실제로 더해지는 수는 0X10 * (포인터가 향하는 오브젝트의 크기) 입니다.
기본적인 C++ 책을 한 번 보세요.
그러면 어떻게해야 0x10만큼만 더할 수 있을까요?
그러면 어떻게해야 0x10만큼만 더할 수 있을까요?
저게 컴파일이 되나요?
저는 이렇게 나오는데요:
아 제가 다른걸 올렸네요 죄송합니다.// g++ -o
아 제가 다른걸 올렸네요 죄송합니다.
$ ./string1
근데 *(ptr+0x10) 하면 제가 원하는 행동이 아니라서 *ptr+0x10 해야할거같은데 맞는 operator+(string,int) 가 없다네요..
어떻게 해야할까요 ㅠㅠ
허...
허...
C++에서도 필요에 따라 printf를 쓸 수 있다지만, 이렇게 오용하는 건 금기입니다!
======
위와 같이 코딩하면 printf에는 string 객체가 넘어가게 됩니다.
상식적으로 C언어의 유산인 printf에 C++ 객체가 넘어가게 되는 상황인데도 돌아가는 것처럼 보이는 이유는 아래와 같습니다.
string은 non-trivial한 copy constructor를 가진 class이기 때문에, 이걸 전달하는 건 "conditionally-supported with implementation-defined semantics"이라고 되어 있습니다.
g++ 매뉴얼을 보면, 이 때 해당 argument는 pass-by-invisible-reference으로 전달한다고 되어 있습니다.
결국 해당 argument를 가리키는 pointer가 넘어가는 형태로 구현이 되고, printf는 str의 내용이 아닌 str의 주소를 받아 출력하게 되는 것이지요.
======
C++ 프로그래밍을 할 때 자신이 어떤 프로그램을 짜려고 하는지 정확히 이해하고, 그걸 C++로 적절히 나타내는 건 온전히 프로그래머의 책임입니다.
단지 컴파일을 무사히 통과하고 잘 실행되는 것처럼 보인다고 해서 프로그래머의 의도대로 짜여졌다는 보장은 어디에도 없는 것이지요.
printf()가 익숙해서 저렇게 썼는데 무슨
printf()가 익숙해서 저렇게 썼는데 무슨 프로그램을 만들려는게 아니라 단순히 궁금해서 테스트하려고 짠거라 암묵적인 룰(?) 같은건 신경을 안썼습니다.
혹시 printf를 사용하면 제가 원하는 내용을 못보나요?
printf는 도깨비 방망이가 아닙니다.
printf는 도깨비 방망이가 아닙니다.
그 이전에, 대체 뭘 보길 원했는지 알려주시는 게 먼저 아닐까요?
본문에 나와있습니다
본문에 나와있습니다
string 은 객체라서 님이 원하는 대로 동작하지
string 은 객체라서 님이 원하는 대로 동작하지 않습니다.
기본 operation 은 함수를 통해 구현되므로 그 동작에 대해서는 명확히 지원하겠지만요
설령 님의 의도대로 동작했다고 해도 모든 시스템에서 동일한 동작을 보장하지도 않을겁니다.
------------------------------------------------------------
ProgrammingHolic
오버로딩으로 해결할 수 없을까요?
오버로딩으로 해결할 수 없을까요?
오버로딩? 오버라이딩?
오버로딩? 오버라이딩?
뭐 할려면 할수 있겠죠..
하지만 위에서 기술한거처럼 모든 시스템이 동일하게 동작 하게 할려고 string 을 만들었는데 그걸 굳이 다시 감쌀 필요가 있을까요?
그럴바엔 그냥 char 의 배열 포인터를 쓰는게 명확하지 않나요?
------------------------------------------------------------
ProgrammingHolic
"string을 사용하면 스택에 저런식으로 저장이
"string을 사용하면 스택에 저런식으로 저장이 되네? 이걸 이렇게 바꾸면 어떻게 될까?" 궁금해서 실험하는 과정에서 막히는게 있어서 질문글을 올린것입니다.
그런데 string을 쓰지말고 char을 쓰는게 어떠냐고 하시면... ㅠㅠ
...
다른 분들이 열심히 답변해 주셨지만, 요약하면:
C++에서 printf의 인자로 객체를 넘기는 건 그냥 틀린 코드라고 생각하는 게 좋습니다.
(...찾아보니까 무슨 Implementation-defined semantics라고 나오는데, 쉽게 말하면 컴파일러 맘대로라는 뜻입니다. 좀 더 풀어쓰면 "배워서 절대 쓸일 없는 코드"라고 할 수 있겠습니다.)
그러니까 "어셈블리로 무슨 코드가 되나요" 같은 질문은, 혹시라도 g++의 코드 생성 로직을 디버깅하는 게 아니라면 열심히 연구해 봤자 아무런 의미가 없습니다. 그냥 이런 코드는 잘못된 코드라고 알고 있으면 99.9% 통합니다.
printf()는 단순히 주소 출력 목적으로
printf()는 단순히 주소 출력 목적으로 사용한것이고, 제가 궁금한건 "printf()의 인자로 객체를 넘길 때 어떤 어셈으로 바뀌는지?"가 아닙니다 ㅠㅠ
cout으로도 하는 방법을 알았다면 그렇게 했을겁니다.
...
주소를 적으려면 그냥 ptr이라고 적어야지요.
*ptr이라고 적으면 매개변수로 주소가 아니라 string object 자체를 보내겠다는 건데, 전혀 다른 겁니다.
위의 예시로 말씀을 드리자면 [20]에 저장되어있는
위의 예시로 말씀을 드리자면 [20]에 저장되어있는 30도 주소니까 %p 로 출력을 해야겠다고 생각했습니다.
...
계속 레벨을 섞고 계신데, string 객체가 저장된 그 위치에 하필이면 문자열 데이터를 가리키는 주소가 있는 건 그냥 string의 내부 implementation detail입니다. C++의 클래스 개념에 대해 차근차근 읽어보시면 아시겠지만, OOP라는 것 자체가 첨부터 "어 메모리에 이런 구조로 저장되어 있으니까 그냥 가져다 쓰면 되겠네?" 하는 게 오만가지 문제를 야기한다는 걸 깨달은 사람들이 "어떻게 하면 이런 삽질을 막을 수 있을까?" 하고 고민해서 나온 겁니다.
일례로 객체 안에 private 멤버 변수가 있으면 그게 메모리에 어떻게 저장되든 말든 그건 구현 디테일이고 그 클래스를 가져다 쓰는 사람이 멋대로 포인터를 캐스팅해서 쓴다고 프로그램이 알아서 잘(?) 동작한다는 보장이 없다는 거죠.
그러니까 C++에서는 똑같은 주소라도 그걸 포인터 변수로 취급하느냐 정수형으로 취급하느냐 string 객체로 취급하느냐가 언어적으로 대단히 중요한 문제이며, 너무 당연한 얘기지만 "어차피 같은 데이터 들어 있는 거 아니에요?" 하고 그냥 섞어 쓰면 여러 가지 못볼 꼴을 보게 됩니다. 이건 C++이라는 언어의 기본 철학(?)이니까 제대로 이해하시고 넘어가는 게 좋겠습니다.
C++의 이런 답답한 점이 맘에 안 들면 그냥 C를 쓰시거나 (뭐 C라고 해서 이런 형식의 프로그래밍을 항상 잘(?) 지원하는 건 아닙니다만..) 기계와 100% 친화적인 어셈블리를 쓰시면 됩니다.
이 분야에 먼저 발을들인 선배로서의 조언은 감사히
이 분야에 먼저 발을들인 선배로서의 조언은 감사히 듣겠습니다만, 제가 본문을 쓴 이유와는 조금 동떨어진 이야기 같습니다..
배워서 쓸일 없는 코드라는 말이 실감나네요 ^^
배워서 쓸일 없는 코드라는 말이 실감나네요 ^^
예전에 하드웨어의 성능을 끌어올리기 위해서 저런식의 하드코어한 기법을 많이 쓰고, 그걸 아는게 지식인 시절이 있었었죠..
그땐 지금처럼 OS 나 시스템이 다양하지 않았기 때문에 거의 어셈블러 수준의 코드를 만드는 경우가 제법 있었는데 말이죠 ^^
요즘은 특정 분야아니면 하드웨어를 때려붓는 시대라 공부할 일이 없는 코드네요
개발자들 성향상 저런걸 파고드는 때가 있기는 한데...
이제는 그냥 남이 잘 만든걸 쓰는게 편하드라구요.. 뭐 필요하면 다시 감싸면 되니까요 ㅋㅋ
------------------------------------------------------------
ProgrammingHolic
물론 이런 코드를 실제로 쓸 일은 없겠습니다만...
물론 이런 코드를 실제로 쓸 일은 없겠습니다만...
나름대로 의미가 있는 일 아니겠습니까? 로우레벨의 세계를 들여다본다는 게.
언어 런타임 라이브러리를 "주어진 것"으로 보고, 그 기능과 용도를 잘 숙지하고 사용하는 것도 중요하지만
가끔은 그 속을 들여다보며 구현상의 디테일을 알아보는 것도 좋지요. 취미삼아서라도. 나중에 어떻게 도움이 될지 누가 압니까?
다만, 적절한 순서로 공부를 하는 게 삽질을 덜 하는 길이겠지요.
그저 익숙하다는 이유로
printf
에string
객체를 때려넣거나, 포인터 산술을 제대로 이해하지 못해*(ptr+(int)0x10)
같은 코드를 쓰거나,*ptr+0x10
같은 코드를 쓰고는 컴파일러가operator+(string,int)
가 없다는데 어떻게 해야 하냐고 묻는 모습에서...기본적으로 C++를 구사하는 데 꼭 필요한 기초를 갖추었다고 보기는 어렵겠습니다.
질문자가 뭘 알고 뭘 모르는지 감도 잡히질 않으니 유의미한 답변을 드릴 수가 없는 것이지요.
로우레벨에 대한 욕심은 일단 좀 접어 두고, 괜찮은 교본을 구해서 순서대로 C++ 공부를 하는 것을 권해드리고 싶습니다만, 귀담아 들으실지 잘 모르겠네요.
뭐, 제가 삽질하는 거 아니니까요. So be it.
제 말이 그말입니다. ^^
제 말이 그말입니다. ^^
예전에는 C 를 side effect 까지 이용해서 자유자재로 구사하던 시절이 있었죠
꼭 side effect 는 아니어도 온갖 트릭과 로우레벨의 지식이 있으면 더 한것도 가능하니까요
------------------------------------------------------------
ProgrammingHolic
C++을 제대로 배운게 아니라 부족하다는 점은
C++을 제대로 배운게 아니라 부족하다는 점은 인지하고있습니다. 참고하겠습니다
다만 궁금한점이 있습니다.
operator+(string, int)가 없어서 오버로딩 해야하냐는 질문이 이상한건지, str도 어처피 주소를 저장하고있을거라 생각을 해서 printf를 사용했는데 그게 이상한건지 이해가 안갑니다.
뭐가 이상한건지 이해가 안가는것도 C++ 기초가 부족해서 그런걸까요..
대충 하시려고 하는 바를 알겠네요.
대충 하시려고 하는 바를 알겠네요.
이렇게 한번 해보시죠.
string str = "aaaaaa";
char *ptr = (char *)&str;
이렇게 해서 ptr 뒤의 한 20 바이트 정도를 출력해 보십시요. 그러면 str 구조가 어느정도 들어날 것이고, 몇개 메모리 값을 바꿔서 시험하시면 될 듯 합니다.
감사합니다! 한번 해보겠습니다
감사합니다! 한번 해보겠습니다
...
https://joellaity.com/assets/long_string.jpg
대충 string object의 메모리 레이아웃의 저렇게 생겼습니다. 그러니까 요는, 저 그림이 정확하다는 게 아니고, str의 시작주소부터 sizeof(string) 내에는 메타 정보 (지금 저장 중인 문자열의 크기 등)과 실제 문자열이 저장된 캐릭터 버퍼의 주소를 담은 포인터 같은 게 들어 있습니다.
string은 STL이니까 어딘가 시스템 인클루드 디렉토리에 template으로 된 구현이 들어 있을 겁니다. 거기 가서 보면 메모리 레이아웃도 어느 정도 알 수는 있어요. 혹은 아마도 -g3 -O0를 주고 컴파일 해서 디버거로 찍어 보면, 디버거가 레이아웃을 보여줄 겁니다.
감사합니다 !
감사합니다 !
댓글 달기