본문 바로가기
Embedded System/소프트웨어 (C,C++)

[F/W] 펌웨어 구현 시 메모리 관리

by MachineJW 2024. 6. 25.

펌웨어란 하드웨어를 직접 접근하고 제어하는 밀접한 관계를 가지고 동작하는 소프트웨어의 일종이다.
그렇기 때문에 일반적인 어플리케이션 소프트웨어와는 다르게 조금 더 신경 써줘야한다고 생각한다.

수정: 스택은 컴파일러가 정해주고 힙은 런타임(프로그램 실행 중 동적으로 바뀜)

1 .스택 오버플로우 (Stack Overflow) 예방

펌웨어의 크기가 커질수록 또는 최적화해야 할 필요가 있다면, 자신에 시스템에 맞는 스택과 힙의 크기를 결정해야 한다.

  • 스택은 메모리 최상단에 위치하여 데이터가 추가될 수록 아래로 커지는 자료구조
  • 힙은 반대로 데이터가 추가될 수록 위로 커지는 자료구조
  • 스택 여유 공간 확인 (모니터링) : FreeRTOS 테스크를 사용중이라면 각 태스크의 스택 사용량을 확인 할 수 있다. 또는 런타임 시 스택의 사용량을 확인하고 디버깅할 수 있겠다.
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>

void setup() {
    Serial.begin(115200);

    // 현재 태스크의 스택 여유 공간 확인
    Serial.print("Stack high water mark: ");
    Serial.println(uxTaskGetStackHighWaterMark(NULL));
}

void loop() {
    // put your main code here, to run repeatedly:
}
  • 재귀 함수 사용의 최소화 : C/C++ 에서 재귀 함수는 호출 깊이에 따라 스택 사용량이 크게 증가 될 수 있다. 재귀함수를 피하거나 반복문으로 대채한다. 재귀함수를 사용해야하는 경우, 재귀의 깊이를 제한하거나 각 호출 시 필요한 스택의 크기를 신중하게 계산한다. 설령 사용하더라도 그럴일이 있었서는 안되겠지만 무한 재귀가 발생하는 일이 있어서는 안된다.
  • 인라인 함수 사용 : 인라인 함수를 사용하면 함수 호출에 따른 스택 프레임 생성이 불필요해진다. 그라나 코드영역(텍스트)의 크기가 증가할 수 있다. 코드 영역에는 실행가능한 명령어가 저장되는 메모리 영역이다.
/* 인라인 함수 선언 */
#include <iostream>

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

int main() {
    int x = 10;
    int y = 20;
    int result = add(x, y); // 인라인 함수 호출
    std::cout << "Result: " << result << std::endl;
    return 0;
}
  • 대형 지역 변수 피하기 : 큰 배열이나 구조체를 지역 변수로 선언하는 대신 동적으로 힙에 할당하는 것이 좋다. C언어의 malloc() 함수나 C++의 new 연산자를 사용하여 동적으로 할당하여 프로그래밍 해보자. (뒤에 서술하겠지만 free(), delete를 사용하여 힙 영역에 할당된 메모리를 해제하는 것은 필수사항이다.)
void someFunction() {
    // 큰 배열을 스택에 할당하는 대신
    // int largeArray[1000];
    
    // 힙에 동적으로 할당
    int* largeArray = (int*)malloc(1000 * sizeof(int));
    if (largeArray == NULL) {
        // 메모리 할당 실패 처리
    }

    // 작업 수행

    free(largeArray); // 메모리 해제 (*필수)
}

 

  • 스택 오버플로우 감지 핸들러 만들기 : 스택 오버플로우를 감지하는 함수(또는 객체)를 만들어 스택 오버플로우가 발생하였을 때 감지 하고 로그출력, 시스템 재부팅 등의 대처를 마련하는 것도 방법이 될수 있다.

2. 메모리 누수 (Memory Leak) 예방

메모리 누수는 C/C++의 malloc()함수나 new 연산자로 부터 동적으로 할당된 힙 메모리가  더 이상 필요하지 않음에도 불구하고 해제되지 않아 시스템의 사용 가능한 메모리가 줄어드는 상황을 말한다. ( 타 메모리 영역과 충돌하는 것만을 의미하지 않는다.) 특히 장시간 실행되는 임베디드 프로그램에서는 메모리 누수 현상에 대해 엄격하게 관리하여야 한다.

  • 힙 (동적 할당 메모리) 할당 후 반드시 해제 : 동적으로 할당된 메모리를 사용한 후에는 반드시 해제한다.
int* ptr = (int*)malloc(sizeof(int) * 10);
if (ptr == NULL) {
    // 메모리 할당 실패 처리
}
// 메모리 사용
free(ptr); // 메모리 반드시 해제
ptr = NULL; // 포인터 초기화

 

  • (클래스 사용시) RAII 패턴 사용: RAII(Resoure Acquisition Is Initalization) 패턴은 생성자와 소멸자를 사용하여 자원의 할당과 해제를 자동으로 관리하는 디자인이다. 객체의 생명 주기에 따라 자원을 관리하게 되며 메모리 누수 뿐만 아니라 기타 자원 누수를 방지할 수 있다.
    #include <iostream>
    #include <fstream>
    
    class FileRAII {
    public:
        FileRAII(const std::string& filename) : file(filename) {
            if (!file.is_open()) {
                throw std::runtime_error("Unable to open file");
            }
            std::cout << "File opened.\n";
        }
    
        ~FileRAII() {
            if (file.is_open()) {
                file.close();
                std::cout << "File closed.\n";
            }
        }
    
        void write(const std::string& data) {
            if (file.is_open()) {
                file << data << std::endl;
            }
        }
    
    private:
        std::ofstream file;
    };
    
    int main() {
        try {
            FileRAII file("example.txt");
            file.write("Hello, RAII!");
            // FileRAII 객체가 스코프를 벗어나면 파일이 자동으로 닫힙니다.
        } catch (const std::exception& e) {
            std::cerr << e.what() << '\n';
        }
        return 0;
    }
    
    /* FileRAII 클래스가 생성될 때 파일을 열고, 객체가 소멸될 때 파일을 자동으로 닫는다. */
#include <iostream>

class MemoryRAII {
public:
    MemoryRAII(size_t size) {
        data = new int[size];
        std::cout << "Memory allocated.\n";
    }

    ~MemoryRAII() {
        delete[] data;
        std::cout << "Memory deallocated.\n";
    }

    int* getData() {
        return data;
    }

private:
    int* data;
};

int main() {
    {
        MemoryRAII mem(100);
        int* data = mem.getData();
        // 메모리 사용
    } // MemoryRAII 객체가 스코프를 벗어나면 메모리가 자동으로 해제됩니다.
    return 0;
}

 // MemoryRAII 클래스가 생성될 때 메모리를 할당하고, 객체가 소멸될 때 메모리를 자동으로 해제합니다.