2021. 11. 1. 20:10ㆍc,c++ 기본/next step
안녕하세요 대학생 개발자입니다.
이어서 C언어 기본에 대해서 얘기를 하려고 합니다.
점프 테이블을 끝으로 마무리를 했던 것 같은데요...
아쉽게도 오늘도 점프 테이블이 등장할 겁니다.
하지만 이번에는 스위치에 대한 점프 테이블은 아니고요.
함수에 대한 점프 테이블인데요.
물론 이 점프 테이블의 경우에는 디버그 빌드에서만 나오는 거긴 하지만...
링킹 타임을 획기적으로 줄이는 방법이기 때문에 이렇게 했답니다.
그럼 우리는 call Foo라는 코드를 전부 점프 테이블로 가게 만들고 점프 테이블의 값만 수정해주면 끝나기 때문이죠.
네 뭐... 점프 테이블은 이런 식으로 생겨먹었습니다.
이상한 다른 함수들이 많은데 제가 따로 뭐 하고 있는 데다가 그냥 점프 테이블 보려고 잠시 디버그로 바꾼 거라서 저런 겁니다...
허허.... 그렇다고요
우선 함수에 대해서 좀 정리를 해볼까 합니다.
우리가 함수를 콜 할 때 그리고 리턴을 만났을 때 어떤 일이 일어나는지 대략적으로 아시는 분들은 많으실 거라고 판단해요.
하지만 구체적으로 함수의 콜이 일어나면 돌아가야 될 코드를 저장하고 jmp를 하는 겁니다.
물론 함수의 파라미터나 이런 것들은 당연히 call에 포함이 안되고 이전에 push나 혹은 레지스터를 이용해서 다 옮겨버리죠 이미.
ret의 경우에도 똑같이 돌아가야 하는 코드를 뽑으면서 점프하는 코드랍니다.
반환 값이 있는 경우엔... 뭐 동일합니다.
레지스터에 넣을 수도 있고.
혹은 반환이 사이즈가 좀 크다면 이전 함수에서 반환 값을 저장할 공간을 할당해 놓고 memcpy를 돌려버리는 경우도 있습니다.
repmov인가 이런 기능을 통해서요.
쫘아아아악 카피를 해버리는 겁니다. 그냥
간단하죠 그래서 이 글을 보신 분들은 만약 진행 중에 갑자기 이상한 곳으로 점프를 한다?
그러면 거의 높은 확률로 ret와 call의 문제일 겁니다.
ret라면 한 스택 위를 확인해야 되기 때문에 어느 함수의 문제인지는 다음 기회에 알 수 있습니다.
적어도 일단 그 함수가 호출되는 스택까지는 갈 수 있지만요...
다른 경우로 call이나 jmp에 의한 경우는 ebp를 통해서 확인하면 가능합니다.
만약 ebp가 잘못되어있다?
그러면 어딘가에서 점프가 잘못된 겁니다...
물론 call 또한 점프를 포함하고 있기 때문에... 점프라고 말씀드린 겁니다.
그래서 이런 장난을 칠 수 있죠
이 코드에서 Test 2를 실행 후 돌아 나온 코드가 또 Test 2인 그런 웃긴 상황을 만들어 낼 수 있죠...ㅋㅋㅋ
다른 케이스로 printf("Test 2");를 끝내고 나왔는데 printf("CCC"); 가 나오는 그런 상황도 나올 수 있겠네요
이런 건 보는 것과 실제로 해보는 게 다르니까요 재밌습니다ㅋㅋㅋ.
저만 그럴 수도 있고요.
네 그리고 함수 호출에 대해서 또 한 가지 정리를 해야 될 것이 있는데요.
바로 호출 규약입니다.
호출 규약이 뭐냐...
뭐... 어떤 식으로 함수를 호출할 거냐에 대한 얘기입니다.
말 그대로 일단 일반적으로 우리가 따로 명시를 하지 않으면 채택되는 방식은 cdecl입니다.
호출자가 파라미터에 대한 정리를 하겠다는 거죠.
반대로 stdcall 같은 것은 호출자가 아니라 피호 출자가 내부에서 파라미터를 정리하고 나오는 건데요...
뭐 윈도의 API들은 대부분 stdcall을 채택하고 있고요.
c, c++ 함수들의 경우와 그리고 아무런 호출 규약의 명시가 없으면 자동으로 cdecl로 된다고 합니다.
그럼 cdecl이 왜 채택이 되었는가를 보면요...
stdcall 같은 경우는 호출자가 push를 해서 파라미터를 넣고 피호 출자가 add를 하는 형식으로 됩니다...
반대로 cdecl의 경우에는 호출자가 push를 한 것은 호출자가 책임지고 add를 하는 겁니다.
이게 논리가 맞다는 거예요 그냥...
누가 뭘 정리하는가는 중요하지 않지만 만든쪽에서 정리하는게 더 좋지 않겠냐 하는 논리인겁니다.
뭐 이걸 섞어서 쓰면 호출 함수와 호출하는 함수가 호출 규약이 다르기 때문에 정리가 안되거나 정리가 2번 돼서 문제가 된다라고 어디서 들은 거 같긴 한데...
저는 뭐 섞어서 써도 잘 되더라고요.
물론 안 되는 케이스도 있을 수도 있습니다.
저는 아직 모든 케이스를 접한 것이 아니기 때문에 확답은 못 드리겠습니다만...
제가 해본 케이스들은 거의 연습 수준이고 실제 저런 케이스가 존재한다고 하니까.
섞어서 쓰는 건 지양해봅시다 그럼.
물론 default 호출 규칙을 바꿀 수 있습니다.
속성 -> 고급 -> 호출 규칙에서 수정 가능한데요 fastcall 같은 경우는... 뭐 스택을 안 쓰고 레지스터를 많이 쓰는 방향으로 가는 거고 뭐 그런 게 있지만 일단 기본은 cdecl입니다.
그리고 마지막으로 재귀 함수입니다...
뭐 재귀 함수는 크게 다룰게 없습니다.
당연히 재귀함수는 강력하죠.
왜냐면 함수를 호출할 때 이전 스택에서 가지고 있던 데이터를 그대로 전달해주니까요.
느낌이 반복문 같다고 할까요 for문을 돌릴 때 i가 0~100까지 가는데 그 i를 매번 다 가지고 가는 느낌이라고 할까요?
그렇기 때문에 강력한 거죠.
스택에 push 하고 반복문을 돌리는 것과 그냥 재귀 함수를 호출하는것 중 어떤게 더 빠른가 라고한다면...
저는 전혀 주저없이 재귀함수를 돌리는 게 빠르다고 할 것 같아요.
왜냐면 우리가 만든 스택에 push 하는 기능이 호출되야되고 그리고 pop 하는 기능이 호출되야되고 매번 그런 경우를 거치는 것보다 그냥 재귀 함수 한번 더 호출하는 게 더 빠르게 친다 이거예요.
물론... 유지보수 같은 경우에는 반복문이 더 고치기는 쉽겠습니다.
그래서 저는 이 케이스에서는 유지보수와 성능이 경합을 해야만 하는 상황이라면 저는 유지보수를 택할 것 같습니다. 물론 성능이 우선시되는 라이브러리나 엔진의 경우에는 조금 다를 수도 있겠네요.
아 이건 논외긴합니다만 그리고 스택을 구현하실 때나 아니면 하나의 함수가 2개 이상의 파라미터를 뱉어내야 될 때는 절대로 함수의 반환 값만을 가지고 하는 행위는 하지 맙시다.
뭐 포인터야 nullptr이 실패다라고 정할 수 있겠지만... int를 반환하는 함수라면 -1이 반환되었을 때 이게 실패인지?
아니면 성공했는데 값이 -1인지 알 수 없으니까요...
이럴 땐 포인터가 있으니까요...
포인터를 씁시다. 아웃 파라미터로
함수에 대해서 하나만 정리를 했는데 꽤나 글이 길어졌어요...
이렇게 길게 정리할 생각은 아니었는데...
째뜬... 이까지 작성하고 다음 글에서 저는 다른 내용으로 찾아오도록 하겠습니다.
그럼 오늘도 봐주셔서 정말 감사드리고 다음에 뵙겠습니다~
'c,c++ 기본 > next step' 카테고리의 다른 글
메모리에 대한 얘기 (0) | 2021.11.07 |
---|---|
바이트 패딩과 캐시에 대한 얘기 (0) | 2021.11.06 |
전처리기, 바이트패딩룰에대한 얘기 (0) | 2021.11.01 |
연산자와 조건문에 대한 얘기 (4) | 2021.10.29 |
변수와 타입, 키워드에관한 얘기 (2) | 2021.10.28 |