Write custom n bits of data in a new binary file using bit-fields

Viewed 269

I've read multiple similar topics about bit-fields, but I don't understand it enough so I could use it. Here's my problem. I have this struct R:

struct R{
   unsigned int opcode: 6;
   unsigned int rs: 5;
   unsigned int rt: 5;
   unsigned int rd: 5;
   unsigned int shamt: 5;
   unsigned int funct: 6;
};

I'm using bit fields so to define that my structure is 32 bits of data. For those who want to know, this structure represent an R type MIPS instruction.

What I want is write that data in a file called result and for that I used this code:

struct R test  = {32,0,11,21,19,0}

FILE *fp = fopen("./result", "rb");
fwrite(&test,sizeof(test),1,result);

With this code, if I run into the console xxd -b result, I expect to see this:

00000000: 00100000 01011000 01110101 00000010

Instead I get

00000000: 00100000 10100000 01110011 00000001

I guess the problem is the fwrite, but I don't really understand.

It is for a homework so I thought about an alternative :

  1. Create an array char sequence[32] which very index simulates 1 bit.
  2. Have a struct of arrays:
struct R{
    char opcode[6];
    char rs[5];
    char rt[5];
    char rd[5];
    char shamt[5];
    char funct[6];
};
  1. Construct my binary sequence with the concatenation of all arrays.
  2. Convert every 8 rows into hex — for example: 00100000 gives 0x20.
  3. Use putc to write into my file.

My alternative is quite long, so is there a way to do it directly, or is there another option that I should know?

2 Answers

What the standard doesn't say

As I noted in a comment,

Bit-fields are an exasperating part of the C standard. Most aspects of their behaviour is implementation-defined. In particular, the mapping of different fields within a unit is implementation-defined, so whether opcode field occupies the most significant 6 bits or the least significant 6 bits is implementation defined.

See C11 §6.7.2.1 Structure and union specifiers, especially ¶10 onwards.

The C standard doesn't stipulate the layout of the bit-fields; it merely says that the implementation must document what it does. If you're finding that when opcode is listed first, it goes in the least significant bits, then so be it; that's what your compiler does. If you want it in the most significant bits, you presumably need to move it to the other end of the structure (and will need to reverse the order of the other fields too). It is all compiler dependent — though the compiler will probably conform to the platform ABI. See GCC documentation on Implementation defined behaviour: Structures, unions, enumerations, and bit-fields, for example. There are places where GCC refers to (and defers to) the platform ABI. You can find the ABI information via Google — what you find is not necessarily very readable, but the information is there.

Some code to analyze your structure

Here is some code based on your structure (and some binary number formatting code):

#include <stdio.h>
#include <assert.h>

static
void format_binary8v(unsigned char x, int n, char buffer[static 9])
{
    assert(n > 0 && n <= 8);
    int start = 1 << (n - 1);
    for (int b = start; b != 0; b /= 2)
    {
        *buffer++ = ((b & x) != 0) ? '1' : '0';
        x &= ~b;
    }
    *buffer = '\0';
}

static
void format_binary32(unsigned int x, char buffer[static 33])
{
    for (unsigned b = 2147483648; b != 0; b /= 2)
    {
        *buffer++ = ((b & x) != 0) ? '1' : '0';
        x &= ~b;
    }
    *buffer = '\0';
}

struct R
{
    unsigned int opcode : 6;
    unsigned int rs : 5;
    unsigned int rt : 5;
    unsigned int rd : 5;
    unsigned int shamt : 5;
    unsigned int funct : 6;
};

static void dump_R(const char *tag, struct R r)
{
    union X
    {
        struct R r;
        unsigned int i;
    };

    printf("%s:\n", tag);
    union X x = { .r = r };
    char buffer[33];
    format_binary32(x.i, buffer);
    printf("Binary: %s\n", buffer);
    format_binary8v(x.r.opcode, 6, buffer);
    printf(" - opcode: %s\n", buffer);
    format_binary8v(x.r.rs, 5, buffer);
    printf(" - rs:      %s\n", buffer);
    format_binary8v(x.r.rt, 5, buffer);
    printf(" - rt:      %s\n", buffer);
    format_binary8v(x.r.rd, 5, buffer);
    printf(" - rd:      %s\n", buffer);
    format_binary8v(x.r.shamt, 5, buffer);
    printf(" - shamt:   %s\n", buffer);
    format_binary8v(x.r.funct, 6, buffer);
    printf(" - funct:  %s\n", buffer);
}

int main(void)
{
    char filename[] = "filename.bin";
    FILE *fp = fopen(filename, "w+b");
    if (fp == NULL)
    {
        fprintf(stderr, "failed to open file '%s' for reading and writing\n", filename);
        return 1;
    }
    //struct R test  = {32, 0, 11, 21, 19, 0};
    struct R test  = { 32, 7, 11, 21, 19, 3 };

    fwrite(&test, sizeof(test), 1, fp);

    dump_R("test - after write", test);

    rewind(fp);
    fread(&test, sizeof(test), 1, fp);
    dump_R("test - after read", test);

    fclose(fp);
    return 0;
}

When run on a MacBook Pro running macOS 10.14.5 Mojave with GCC 9.1.0, I get:

test - after write:
Binary: 00001110011101010101100111100000
 - opcode: 100000
 - rs:      00111
 - rt:      01011
 - rd:      10101
 - shamt:   10011
 - funct:  000011
test - after read:
Binary: 00001110011101010101100111100000
 - opcode: 100000
 - rs:      00111
 - rt:      01011
 - rd:      10101
 - shamt:   10011
 - funct:  000011

And the raw binary output file:

$ xxd -b filename.bin
00000000: 11100000 01011001 01110101 00001110                    .Yu.
$

My interpretation is that on my machine, the data for the opcode bit-field is in the least significant 6 bits of the storage unit, the data for funct bit-field is in the most significant 6 bits, and the other elements are in between. This is clear when looking at the 32-bit value. The way xxd -b splits it up takes more explaining:

  • The first byte is the least significant byte — Intel architecture.
  • It contains all 6 bits of opcode in its least significant bits; it contains the two least significant bits of rs, too, in its most significant bits.
  • The second byte contains the three most significant bits of rs as its least significant bits, and all 5 bits from rt as its most significant bits.
  • The third bytes contains all 5 bits of rd in its least significant bits and the 3 least significant bits of shamt in its most significant bits.
  • The fourth and most significant bye contains the 2 most significant bits of shamt in its least significant bits, and all 6 bits of funct in its most significant bits.

It's all a bit mind-blowing!

When I revert to your values for the test structure (struct R test = {32, 0, 11, 21, 19, 0};), I get:

test - after write:
Binary: 00000010011101010101100000100000
 - opcode: 100000
 - rs:      00000
 - rt:      01011
 - rd:      10101
 - shamt:   10011
 - funct:  000000
test - after read:
Binary: 00000010011101010101100000100000
 - opcode: 100000
 - rs:      00000
 - rt:      01011
 - rd:      10101
 - shamt:   10011
 - funct:  000000

and

00000000: 00100000 01011000 01110101 00000010                     Xu.

Your hardware and/or compiler is different from mine; it may have different rules for the layout of the bit-fields.

Note that this code assumes without testing that unsigned or unsigned int is a 32-bit quantity. If you're on a system where that doesn't hold true, you'll need to revise the code to use types like uint32_t and uint8_t, etc, as found in <stdint.h> (and format specifiers as found in <inttypes.h>).

Refined code

This code is better organized in various ways than the original code.

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct R
{
    unsigned int opcode : 6;
    unsigned int rs     : 5;
    unsigned int rt     : 5;
    unsigned int rd     : 5;
    unsigned int shamt  : 5;
    unsigned int funct  : 6;
};

static void test_r(const char *tag, struct R r, FILE *fp);
static void run_xxd(const char *file);

int main(void)
{
    char filename[] = "filename.bin";
    FILE *fp = fopen(filename, "w+b");
    if (fp == NULL)
    {
        fprintf(stderr, "failed to open file '%s' for reading and writing\n", filename);
        return 1;
    }

    struct R r[]  =
    {
        { 32,  0, 11, 21, 19,  0 },
        { 32,  7, 11, 21, 19,  3 },
        {  6, 21, 10, 14, 10,  8 },
    };
    enum { NUM_R = sizeof(r) / sizeof(r[0]) };

    for (int i = 0; i < NUM_R; i++)
    {
        char name[16];
        snprintf(name, sizeof(name), "r%d", i+1);
        test_r(name, r[i], fp);
    }

    fclose(fp);

    run_xxd(filename);

    return 0;
}

static void run_one_xxd(const char *command, const char *filename)
{
    char cmd[256];
    snprintf(cmd, sizeof(cmd), "%s %s", command, filename);
    printf("\nCommand: %s\n", cmd);
    fflush(stdout);
    system(cmd);
    putchar('\n');
}

static void run_xxd(const char *filename)
{
    run_one_xxd("xxd -c 4 -b     ", filename);
    run_one_xxd("xxd -c 4 -g 1 -u", filename);
}

static void format_binary8v(unsigned char x, int n, char buffer[static 9]);
static void format_binary32(unsigned x, char buffer[static 33]);
static void dump_bitfield(int nbits, unsigned value, const char *name);
static void dump_bytes(const char *tag, struct R r);
static void dump_R(const char *tag, struct R r);

static void test_r(const char *tag, struct R r, FILE *fp)
{
    char buffer[32];
    long offset = sizeof(struct R);
    putchar('\n');
    fwrite(&r, sizeof(r), 1, fp);
    snprintf(buffer, sizeof(buffer), "%s - after write", tag);
    dump_R(buffer, r);
    fseek(fp, -offset, SEEK_CUR);
    struct R s;
    fread(&s, sizeof(s), 1, fp);
    fseek(fp, 0, SEEK_CUR);             // Ready for reading or writing!
    snprintf(buffer, sizeof(buffer), "%s - after read", tag);
    dump_R(buffer, s);
    /* Safe regardless of whether struct R uses all bits in its storage unit */
    assert(r.opcode == s.opcode);
    assert(r.rs     == s.rs    );
    assert(r.rs     == s.rs    );
    assert(r.rs     == s.rs    );
    assert(r.shamt  == s.shamt );
    assert(r.funct  == s.funct );
    /* Only safe because struct R uses all bits of its storage unit */
    assert(memcmp(&r, &s, sizeof(struct R)) == 0);
}

static void dump_R(const char *tag, struct R r)
{
    printf("%s:\n", tag);
    dump_bytes("Binary", r);
    dump_bitfield(6, r.opcode, "opcode");
    dump_bitfield(5, r.rs,     "rs");
    dump_bitfield(5, r.rt,     "rt");
    dump_bitfield(5, r.rd,     "rd");
    dump_bitfield(5, r.shamt,  "shamt");
    dump_bitfield(6, r.funct,  "funct");
}

static void dump_bytes(const char *tag, struct R r)
{
    union X
    {
        struct R r;
        unsigned i;
    };
    union X x = { .r = r };
    char buffer[33];
    printf("%s: 0x%.8X\n", tag, x.i);
    format_binary32(x.i, buffer);
    //printf("%s: MSB %s LSB\n", tag, buffer);
    printf("%s: MSB", tag);
    for (int i = 0; i < 4; i++)
        printf(" %.8s", &buffer[8 * i]);
    puts(" LSB (big-endian)");
    printf("%s: LSB", tag);
    for (int i = 0; i < 4; i++)
        printf(" %.8s", &buffer[8 * (3 - i)]);
    puts(" MSB (little-endian)");
}

static void dump_bitfield(int nbits, unsigned value, const char *name)
{
    assert(nbits > 0 && nbits <= 32);
    char vbuffer[33];
    char nbuffer[8];
    snprintf(nbuffer, sizeof(nbuffer), "%s:", name);
    format_binary8v(value, nbits, vbuffer);
    printf(" - %-7s  %6s  (%u)\n", nbuffer, vbuffer, value);
}

static
void format_binary8v(unsigned char x, int n, char buffer[static 9])
{
    assert(n > 0 && n <= 8);
    int start = 1 << (n - 1);
    for (int b = start; b != 0; b /= 2)
    {
        *buffer++ = ((b & x) != 0) ? '1' : '0';
        x &= ~b;
    }
    *buffer = '\0';
}

static
void format_binary32(unsigned x, char buffer[static 33])
{
    for (unsigned b = 2147483648; b != 0; b /= 2)
    {
        *buffer++ = ((b & x) != 0) ? '1' : '0';
        x &= ~b;
    }
    *buffer = '\0';
}

It produces the output:

r1 - after write:
Binary: 0x02755820
Binary: MSB 00000010 01110101 01011000 00100000 LSB (big-endian)
Binary: LSB 00100000 01011000 01110101 00000010 MSB (little-endian)
 - opcode:  100000  (32)
 - rs:       00000  (0)
 - rt:       01011  (11)
 - rd:       10101  (21)
 - shamt:    10011  (19)
 - funct:   000000  (0)
r1 - after read:
Binary: 0x02755820
Binary: MSB 00000010 01110101 01011000 00100000 LSB (big-endian)
Binary: LSB 00100000 01011000 01110101 00000010 MSB (little-endian)
 - opcode:  100000  (32)
 - rs:       00000  (0)
 - rt:       01011  (11)
 - rd:       10101  (21)
 - shamt:    10011  (19)
 - funct:   000000  (0)

r2 - after write:
Binary: 0x0E7559E0
Binary: MSB 00001110 01110101 01011001 11100000 LSB (big-endian)
Binary: LSB 11100000 01011001 01110101 00001110 MSB (little-endian)
 - opcode:  100000  (32)
 - rs:       00111  (7)
 - rt:       01011  (11)
 - rd:       10101  (21)
 - shamt:    10011  (19)
 - funct:   000011  (3)
r2 - after read:
Binary: 0x0E7559E0
Binary: MSB 00001110 01110101 01011001 11100000 LSB (big-endian)
Binary: LSB 11100000 01011001 01110101 00001110 MSB (little-endian)
 - opcode:  100000  (32)
 - rs:       00111  (7)
 - rt:       01011  (11)
 - rd:       10101  (21)
 - shamt:    10011  (19)
 - funct:   000011  (3)

r3 - after write:
Binary: 0x214E5546
Binary: MSB 00100001 01001110 01010101 01000110 LSB (big-endian)
Binary: LSB 01000110 01010101 01001110 00100001 MSB (little-endian)
 - opcode:  000110  (6)
 - rs:       10101  (21)
 - rt:       01010  (10)
 - rd:       01110  (14)
 - shamt:    01010  (10)
 - funct:   001000  (8)
r3 - after read:
Binary: 0x214E5546
Binary: MSB 00100001 01001110 01010101 01000110 LSB (big-endian)
Binary: LSB 01000110 01010101 01001110 00100001 MSB (little-endian)
 - opcode:  000110  (6)
 - rs:       10101  (21)
 - rt:       01010  (10)
 - rd:       01110  (14)
 - shamt:    01010  (10)
 - funct:   001000  (8)

Command: xxd -c 4 -b      filename.bin
00000000: 00100000 01011000 01110101 00000010   Xu.
00000004: 11100000 01011001 01110101 00001110  .Yu.
00000008: 01000110 01010101 01001110 00100001  FUN!

Command: xxd -c 4 -g 1 -u filename.bin
00000000: 20 58 75 02   Xu.
00000004: E0 59 75 0E  .Yu.
00000008: 46 55 4E 21  FUN!

I've tested your example and I'm getting the expected results:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int binpr(unsigned char Byte, FILE *f)
{
    char buf[8];
    for(int i=0; i<8; i++) buf[i]=(char)(((Byte>>(7-i))&1)+'0');
    return (int)+fwrite(buf,1,8,f);
}
struct R{

   unsigned int opcode: 6;
   unsigned int rs: 5;
   unsigned int rt: 5;
   unsigned int rd: 5;
   unsigned int shamt: 5;
   unsigned int funct: 6;
};
int main()
{

    struct R test  = {32,0,11,21,19,0};
    system(": > result"); //rb requires that the file already exists
    FILE *fp = fopen("./result", "rb+");
    if(!fp) return perror("fopen"),1;
    if(1!=fwrite(&test,sizeof(test),1,fp)) return perror("fwrite"),1;
    rewind(fp);
    char buf[sizeof(struct R)];
    if(1!=fread(&buf,sizeof(buf),1,fp)) return perror("fread"),1;
    fputs(" ",stdout); if(0!=memcmp(buf,&test,sizeof test)) abort();
    for(size_t i=0; i<sizeof(test); i++) { binpr(*((unsigned char*)&test+i),stdout); fputs(" ",stdout); } puts("");
    system("xxd -b result |cut -d: -f2");

     /*OUTPUT:*/
     /*00100000 01011000 01110101 00000010 */
     /*00100000 01011000 01110101 00000010                     Xu.*/
}

Note that to open the file for updates and reading, you need "rb+" instead of just "rb". Otherwise you'll get errors on fwrite (which you won't see because you aren't doing any error checking).

(Your compiler laying down bitfields in an unusual way is also a possibility, although it's probably less likely than an erroneously specified fopen flag.)

Related