Dereferencing a NULL pointer (or some address outside your current address space, often in virtual memory) in C is not an exception, but some undefined behavior (often a segmentation fault). You really should avoid UB.
By definition of undefined behavior, we cannot explain it without going down into very specific implementation details (compiler, runtime, optimization, ASLR, machine code, phase of the moon, ...).
The malloc library function can (and does) fail. You should always test it, at least as:
 int *b = malloc(sizeof(int));
 if (!b) { perror("malloc of int"); exit(EXIT_FAILURE); }; 
To trigger failure of malloc (but very often the first few calls to malloc would still succeed) you might lower the available address space to your program. On Linux, use ulimit in the parent shell, or call setrlimit(2).
BTW, you could even link with your own malloc which is always failing:
  // a silly, standard conforming, malloc which always fail:
  void* malloc(size_t sz) {
    if (sz == 0) return NULL;
    errno = ENOMEM;
    return NULL;
  }
The C programming language does not have exceptions. C++ (and Ocaml, Java, ....) does (with catch & throw statements). Raising an exception is a non-local change of control flow. In standard C you might use longjmp for that purpose. In C++ dereferencing a nullptr is UB (and does not raise any null-pointer exception which does not exist in C++).