Search code examples
cgccstructbmp

Keep GCC from altering size of structs (.BMP file header)


I'm currently trying to create a .BMP file. The BMP header is, by my definition, 54 bytes long. The code compiles and everything, but when trying to open the file, I get a "incorrect header format" error.

If I do a sizeof(structtype) I get 56 bytes instead of the defined 54 - and if I initialize a struct with values, then do a sizeof(newStruct), I get 8 bytes. Since I need the exact 54 bytes to be written into the file, this is terrible.

Is there a way to keep the GCC from altering the struct size this way?

Here's the definition of the structs:

typedef struct
{
  uint16_t typeSignature; // = "BM"
  uint32_t filesize;     //filesize in Bytes
  uint32_t reserved;     // = 0 for this program
  uint32_t headerOffset; // = 54
} BmpFileHeader;

typedef struct
{
  uint32_t infoHeaderSize;    //size of header in byte. ( = 40)
  uint32_t Width;             //width of file in pixels
  uint32_t Height;            // height of file in pixels

  uint16_t Colors;       //colorbits per pixel (24 for 3byte RGB)
  uint16_t bitsPerPixel;
  uint32_t Compression;       //compression mode; 0 for uncompressed.
  uint32_t SizeImg;         //if biCompress = 0, =0. Else: filesize.

  uint32_t xPelsPerMeter;     // for output device;
  uint32_t yPelsPerMeter;     // 0 for this program

  uint32_t ColorsUsed;      // Colours used; = 0 for all
  uint32_t ColorsImportant; // num. of used colours, 0 for all
} BmpInfoHeader;

typedef struct
{
  BmpFileHeader fileheader;
  BmpInfoHeader infoheader;
} bitmapHead; // sizeof = 56

and here the function which initializes a new Header:

bitmapHead* assembleHeader(int compCount)
{
  bitmapHead* newHeader = (bitmapHead*) calloc(1, 54);  
  newHeader->fileheader.typeSignature = 0x4D42;
  newHeader->fileheader.filesize = (compCount*100*51*3 + 54);
  newHeader->fileheader.reserved = 0;
  newHeader->fileheader.headerOffset = 54;


  newHeader->infoheader.infoHeaderSize = 40;
  newHeader->infoheader.Width = 100*compCount;
  newHeader->infoheader.Height = 51;
  newHeader->infoheader.Colors = 1; 
  newHeader->infoheader.bitsPerPixel = 21;
  newHeader->infoheader.Compression = 0;
  newHeader->infoheader.SizeImg = compCount*100*51*3;
  newHeader->infoheader.xPelsPerMeter = 0;
  newHeader->infoheader.yPelsPerMeter = 0;
  newHeader->infoheader.ColorsUsed = 0;
  newHeader->infoheader.ColorsImportant = 0;
  printf("%lu \n", sizeof(newHeader)); // This gives me 8. 
  return newHeader;
}

Solution

  • This is a case of "don't do that then." Specifically, never attempt to lay out a C struct which matches a byte pattern defined by an external specification. The C standard is not interested in cooperating with this, and while some compilers attempt to make it sort-of possible, it's more trouble than it's worth. (Read up on Ada representation clauses, if you're interested in seeing what a language that takes this problem seriously looks like.)

    You can define a struct to represent the BMP header in memory, but the thing you read from/write to disk should be an array of uint8_t. Ya rly. You have to write functions to explicitly convert between the in-memory representation and the on-disk representation. These are also a good place to do any checking that may be necessary, and deal with endianness.

    Worked example for your BmpFileHeader:

    // Fixed fields not represented in in-memory header.
    typedef struct
    {
        uint32_t filesize;     // total size of file in bytes
        uint32_t headerOffset; // offset from beginning of file to end of headers
                               // (normally 54)
    } BmpFileHeader;
    
    #define BMP_FILE_HEADER_MAGIC_1  0
    #define BMP_FILE_HEADER_MAGIC_2  1
    #define BMP_FILE_HEADER_FILESIZE 2
    #define BMP_FILE_HEADER_RESERVED 6
    #define BMP_FILE_HEADER_HDROFF   10
    #define BMP_FILE_HEADER_SIZE     14
    typedef uint8_t BmpFileHeaderOnDisk[BMP_FILE_HEADER_SIZE];
    
    uint32_t
    le32_to_cpu(uint8_t *p)
    {
        return ((((uint32_t)p[0]) <<  0) | 
                (((uint32_t)p[1]) <<  8) |
                (((uint32_t)p[2]) << 16) |
                (((uint32_t)p[3]) << 24));
    }
    
    // Returns true if header is successfully parsed.
    bool
    load_bmp_file_header(BmpFileHeaderOnDisk ondisk, BmpFileHeader *inmem)
    {
        if (ondisk[BMP_FILE_HEADER_MAGIC_1] != 'B' ||
            ondisk[BMP_FILE_HEADER_MAGIC_2] != 'M' ||
            le32_to_cpu(ondisk + BMP_FILE_HEADER_RESERVED) != 0)
            return false; // not a BMP file
    
        inmem->filesize = le32_to_cpu(ondisk + BMP_FILE_HEADER_FILESIZE);
        inmem->headerOffset = le32_to_cpu(ondisk + BMP_FILE_HEADER_HDROFF);
        return true;
    }
    

    Going the opposite way, and the treatment of the "info" header, left as exercises.