소켓 입/출력 버퍼의 크기를 확인하려면 다음과 같은 함수를 사용하면 된다.

 

getsockopt
//윈속 초기화
WSADATA wsa = { 0 };
if (::WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
	puts("ERROR: 윈속을 초기화 할 수 없습니다.");
	return 0;
}

//소켓 생성
SOCKET hSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
{
	puts("ERROR: 소켓을 생성할 수 없습니다.");
	return 0;
}

//소켓의 '송신' 버퍼의 크기를 확인하고 출력한다.
int nBufSize = 0, nLen = sizeof(nBufSize);
if (::getsockopt(hSocket, SOL_SOCKET,
	SO_SNDBUF, (char*)&nBufSize, &nLen) != SOCKET_ERROR)
	printf("Send buffer size: %d\n", nBufSize);

//소켓의 '수신' 버퍼의 크기를 확인하고 출력한다.
nBufSize = 0;
nLen = sizeof(nBufSize);
if (::getsockopt(hSocket, SOL_SOCKET,
	SO_RCVBUF, (char*)&nBufSize, &nLen) != SOCKET_ERROR)
	printf("Receive buffer size: %d\n", nBufSize);

//소켓을 닫고 종료.
::closesocket(hSocket);
::WSACleanup();

 

위 코드 처럼 getsockopt 함수를 이용해 소켓의 입력/출력 버퍼의 크기를 확인할 수 있다.

 

https://learn.microsoft.com/ko-kr/windows/win32/api/winsock/nf-winsock-getsockopt

 

getsockopt 함수(winsock.h) - Win32 apps

getsockopt 함수(winsock.h)는 소켓 옵션을 검색합니다.

learn.microsoft.com

 

'강의 정리 > 인프런' 카테고리의 다른 글

[인프런] 시큐어 코딩  (0) 2025.03.31
[인프런] 쉘 코드  (0) 2025.03.31
[인프런] 쓰레드 생성 및 실행  (0) 2025.03.31
[인프런] 함수 포인터  (0) 2025.03.22
[인프런] inline 함수와 컴파일러 최적화  (0) 2025.03.17
시큐어 코딩은 소프트웨어 개발 과정에서 보안 취약점을 최소화하여 악의적인 공격으로부터 시스템을 보호하기 위해
안전한 코드를 작성하는 방법을 말한다.

 

📌 시큐어 코딩의 주요 원칙

  • 입력 검증 : 모든 '입력'을 신뢰하지 말고 검증해야한다.
  • 출력 인코딩 : 출력 데이터를 인코딩하여 악의적인 코드 실행을 방지해야한다.
  • 최소 권한 원칙 : 시스템 내에서 각 사용자나 프로세스가 최소한의 권한만을 가지고 작업할 수 있도록 설정해야한다.
  • 암호화 : 중요한 데이터는 전송 중에나 저장 중에 암호화하여 유출을 방지해야한다.
  • 에러 처리 및 로깅 : 시스템 오류 메시지나 스택 트레이스를 외부에 노출하지 않도록 하고, 필요한 에러 로그는 안전하게 기록하여 디버깅과 추적이 가능하게 해야한다.
쉘코드는 보통 악성 코드에서 사용되는 용어로, 컴퓨터 시스템의 취약점을 이용해 공격자가 원격으로 코드를 실행하거나
특정 동작을 강제로 실행할 수 있도록 하는 작은 코드를 말한다.

 

📌 쉘 코드 작동 원리

쉘코드는 보통 시스템의 메모리 공간에 삽입되어, 시스템 취약점을 악용하여 악성 코드를 실행하게 만든다.

 

  • 윈도우 탐색기처럼 다른 프로그램을 실행 할 수 있는 코드조각을 말함
  • 핵심은 다른 프로그램을 실행 시키는 것
  • 특정 프로세스가 스스로 새 프로세스를 생성할 경우 권한이 그대로 복제된다.

 

  • 한 프로세스는 최소 1개 이상의 쓰레드를 갖는다.
  • 쓰레드는 개별화된 흐름( 문맥 )과 전용 스택을 갖는 실행의 단위다.
  • 모든 쓰레드는 자신이 속한 프로세스의 가상 메모리 공간을 공유한다.

 

 

함수 포인터는 함수를 가리키는 포인터로, 특정 함수의 주소를 저장하고 이를 통해 해당 함수를 호출할 수 있다.

 

  • 함수의 이름은 그 자체가 주소다.
  • 함수호출 연산자의 피연산자는 함수 포인터와 같은 형식이여야 한다.
  • 실행 코드가 저장된 메모리를 가리키는 포인터다.

 

이름이 주소인 경우는 배열 또한 마찬가지인데, 

배열은 프로세스에서 데이터 영역에 저장된다. 데이터 영역은 읽고 쓰기가 가능하며, 실행은 불가능하다.

 

반면 실행 코드가 담겨 있는 실행 코드 영역에서는 읽기와 실행이 가능하며 쓰기는 불가능하다.

만약 실행 코드 영역이 쓰기가 가능하면 해당 실행 코드가 위조 혹은 변조가 되기 때문

 

운영체제는 실행코드의 영역과 데이터의 영역을 강력하게 구별한다.

 


 

📌 함수 포인터 형식

반환형 (*이름)(매개변수, ... )

 

int add(int a, int b) {
	return a + b;
}

 

위와 같은 함수가 있을때, add 함수를 함수포인터에 담으려면 다음과 같이 선언하고 add 함수를 담으면 된다.

 

int (*pfAdd)(int, int) = add;

 


#include<stdio.h>

int add(int a, int b) {
	return a + b;
}

int main() {
	int result = 0;
	result = add(1, 2);

	printf("Result: %d\n", result);

	int (*pfAdd)(int, int) = add;
	result = pfAdd(3, 4);
	printf("Result: %d\n", result);

	return 0;
}

 

위와 같이 코드를 구성하고 실행한 후 어셈블리어를 확인해보자.

 

Release에서 살펴보면, 함수 포인터 역시 함수를 호출하는 변수이기 때문에 최적화가 될 경우 함수 포인터를 콜해도 어셈블리에서는 call 하지 않는 것을 확인 할 수 있다.

 

 

반면 Debug에서는 pfAdd에 add 함수의 주소를 담고, pfAdd를 call하는 것을 확인 할 수 있다.

 

 

 

 

inline함수는 C++에서 사용되는 키워드로, 함수 호출 오버헤드를 줄이기 위해 함수의 기계어 코드가 호출된 위치에 직접 삽입되도록 컴파일러에게 요청하는 역할을 한다.

 

  • 함수 호출에 필요한 스택 프레임 생성, 스택 증/감 매개변수 복사 등 부가적 코드 수행에 따른 오베헤드 발생을 줄여 속도를 향상시킨다.
  • 불필요한 오버헤드 제거를 위해 코드 상 존재하는 Callee의 코드를 Caller에 포함시켜 실제로는 호출하지 않도록 해 최적화 한다.
  • 다만 inline 키워드를 사용했다고 해서 반드시 되는 것은 아니고, 컴파일러가 결정한다.

 

📌 변수의 종류

여러가지 형태로 변수의 종류를 나누는데, 그 중 접근성을 기준으로 변수를 나누면

지역 변수와 전역변수 2가지로 나눌 수 있다.

 

위 둘을 나눌 때 분할 컴파일을 안한다는 것을 대전제를 둬야 한다.

무슨 말인지 예시를 들어보자.

 

main.c에서 main이 add란 함수를 부르거나 어떤 변수에 접근하는 모든 것이 main.c 파일안에 모든 코드를 기술한다.

즉, 하나의 소스 코드 파일을 가지고 모든 작업을 수행하는 것. 다시 말해 분할 컴파일이라는 걸 하지 않는다.

만약 main.c에서 모든 작업을 처리하지 않고, main.c, add.c로 파일을 분할해

컴파일을 하면 각각 구별된 .obj 파일을 생성하기 때문에 전역변수라고 하는 것은 하나의 파일에만 적용이 된다.

 

📌 외부 변수 선언

외부 변수 선언이란, 다른 파일 또는 코드 블록에서 정의된 변수를 현재 파일이나 코드 블록에서 사용할 수 있도록
선언하는 것을 의미한다.

 

외부 변수는 다른 파일에서 전역 변수를 사용할 때 필요하다. 한 파일에서 변수를 정의하고,

다른 파일에서는 extern 키워드를 사용해 참조하는 방식이다.

 

🔹 변수 정의 

  • 변수를 메모리에 할당하고 초기값을 결정

🔹 변수 선언

  • 변수가 존재한다는 사실만 알리고, 실제 메모리 할당은 하지 않는다.

 

✅ 외부 변수 선언의 특징

  • 메모리 할당이 없다.
  • 다른 파일의 전역 변수를 사용이 가능하다.
  • 초기화가 불가능하다.

 

예제를 살펴보자.

 

test.c 파일

#include<stdio.h>

int main(void)
{
     int result = add(3,4);
     printf("Result: %d\n", result);
     return 0;
}

 

add.c 파일 ( add 함수 정의 )

int add(int a, int b)
{
     int result = a + b;
     return result;
}

 

위 상태로 빌드하면 warning이 출력된다. ( c++ 에서는 에러 )

 

 

컴파일러가 test.c에 있는 add()를 보고 함수를 확인한 후 add.c에 있는 add() 함수를 발견하고 알아서 이은 것이다.

경고가 안나오게 하려면 사용하는 쪽에서 함수를 선언해주면 된다.

 

test.c 수정 ( add 함수 선언 )

#include<stdio.h>

int add(int, int); // add 함수 선언

int main(void)
{
     int result = add(3,4);
     printf("Result: %d\n", result);
     return 0;
}

 

위 코드처럼 함수를 선언해주면 더이상 warning이 발생하지 않는다.

 

이처럼 함수 선언을 따로 모은 파일을 헤더(.h)라고 부른다.

add 함수를 선언하는 헤더 파일을 생성

 

add.h

#pragma once

int add(int, int);

 

헤더라고 부르는 파일은 일반적으로 함수의 선언이 들어가 있다.

정의를 넣으면 헤더를 include 할때마다 중복되기 때문에 에러가 난다.

 

test.c 수정 ( add.h 을 include 한다 )

#include<stdio.h>
#include"add.h"

int main(void)
{
    int result = add(3, 4);
    printf("Result: %d\n", result);
    return 0;
}

 


외부 변수

add.c 

extern int result;

int add(int a, int b)
{
     result = a + b;
     return result;
}

 

test.c

#include<stdio.h>
#include"add.h"

int main(void)
{
    add(3,4);    
    return 0;
}

 

add.c에 extern을 이용해 result을 선언하고 test.c를 위와 같이 수정한후 빌드하면

링크에러가 난다. 

 

외부 변수로 선언한 result를 확인할 수 없다는 것인데, 

extern을 이용해 앞에 붙이면 반드시 해당 변수를 다른곳에서 정의를 해주어야하기 때문이다.

 

즉 test.c에서 int result = 5; 라고 정의를 해주어야 링크에러가 잡힌다.

 

#include<stdio.h>
#include"add.h"

int result = 5;

int main(void)
{
    add(3,4);  
    
    printf("Result: %d\n", result);
    return 0;
}

 

위와 같이 구성하고 출력하면 7이 출력된다.

형한정어란, 변수에 적용하는 문법으로 컴파일러 최적화에 직접적인 영향을 준다.

📌 핵심 예약어

  • const ( 상수화 )
  • volatile ( 최적화 대상에서 제외 )

📌 심볼릭 상수

  • 특정 상수에 이름을 부여해 그 의미가 명확하도록 표현하는 것을 말한다.
  • 형한정어 const로 구현한다.
  • #define 전처리기로 구현한다.  ( 컴파일 전, 전처리 단계에서 상수화 처리 )

 

📌 컴파일러 최적화

컴파일러가 컴파일 과정에서 코드 변환 및 재배치를 수행하는 것을 말한다.

 

🔹 컴파일러 최적화의 목적

  • 실행 속도를 개선
  • 코드 크기를 감소
  • 메모리 사용을 최적화

 

컴파일러 최적화는 일반적으로 프로그램을 빌드( 디버그 모드 )하면 작동하지 않는다.

릴리즈 모드로 빌드를 하게 되면 이때 최적화를 실시한다.

 

불필요한 코드를 제거하는 최적화 방식 적용시 특정 변수에 대한 의존관계를 분석해 최적화가 가능하다.

포인터는 최적화에 방해 요소가 될수 있다. 주소만 있고, 해당 포인터가 가리키고 있는 대상의 크기를 알 수 없기 때문

 

컴파일러 최적화를 돕는 코드가 좋은 코드라고 생각할 수 있다.

개발자 입장에서는 const를 적절한 곳에 붙여 최적화를 도와주면 된다.

 

🔹 예시

int a = 0;

for(int i=0; i< 10; i++)
{
    a = 5;
}

printf("a: %d\n", a);

 

위와 같은 코드가 있을때, 릴리즈 모드로 빌드하면 어떤일이 일어날까?

 

컴파일러는 for문을 여러번 돌아도 a에 5만 넣고 값이 변하지 않기 때문에

for문 자체를 삭제하고 a에 5를 넣고 끝낸다.

 

컴파일러가 최적화를 하지 못하게 하려면 어떻게 해야할까?

그때 필요한 형한정어가 volatile이다.

 

volatile int a = 0; // volatile를 붙여서 최적화에서 제외시킨다.

for(int i=0; i< 10; i++)
{
    a = 5;
}

printf("a: %d\n", a);

 

위와 같이 volatile을 붙이고 빌드하면 다음과 같이 코드가 생성된다.

 

 

어셈블리어를 살펴보면 for문도 반복하는 것을 확인할 수 있다.

함수 호출 규약은 함수호출로 증가한 스택 메모리 사용 및 관리에 관련된 규칙을 말한다.

 

📌 종류

⭐ cdecl ( C Declaration )

  • 매개변수를 오른쪽에서 왼쪽 순서로 스택에 저장한다.
  • 호출자( Caller )가 스택을 정리한다.
  • C 언어의 기본 규약이다.

⭐ stdcall ( Standard call )

  • 매개변수를 오른쪽에서 왼쪽 순서로 스택에 저장한다.
  • 호출된 함수( Callee )가 스택을 정리한다.

⭐ fastcall ( Fast Call )

  • 처음 두 개의 인자는 레지스터에 저장하고, 나머지는 스택에 저장한다.
  • 호출된 함수( Callee )가 스택을 정리한다.
주소에 의한 전달과 참조에 의한 전달의 코드를 살펴보자.

 

📌 주소에 의한 전달

#include<stdio.h>

void swap(int *pA, int *pB)
{
    int nTmp = *pA;
    *pA = *pB;
    *pB = nTmp;
    return;
}

int main(void)
{
    int x = 3, y = 4;
    swap(&x, &y);
    printf("%d, %d\n", x, y);
    return 0;
}

 

 

📌 참조에 의한 전달

#include<iostream>

using namespace std;

void swap(int& pA, int& pB)
{
	int temp = pA;
	pA = pB;
	pB = temp;
	return;
}

int main(void)
{
	int a = 10;
	int b = 20;

	swap(a, b);
	cout << "a = " << a << ", b = " << b << endl;	

	return 0;
}

 

주소에 의한 전달은 c로 작성했고, 참조에 의한 전달은 c++로 작성했다.

실제 어셈블리로 매개변수가 전달되는 부분을 확인하면 어떤 모습일까?

 

c 주소에 의한 매개변수 전달

 

c++ 참조에 의한 매개변수 전달

 

어셈블리 수준에서 두 코드를 보면 똑같이 ptr 즉, 주소로 접근해서 데이터를 처리하는 것을 확인할 수 있다.

이처럼 주소에 의한 전달과 참조에 의한 전달은 모두 주소를 이용해서 데이터를 처리하는 것이므로 차이가 없다.

 

어떤 의미에서는 포인터가 c++ 이나 JAVA에서 참조자가 되어주는 것이라고도 볼 수 있겠다.

+ Recent posts