The crucial point that has to be made about regular expressions is that they can't count; so a single regexp can't cope with tags that are nested arbitrarily deep. If there is a fixed limit, it can be done, but really only in principle; a solution that would also work for arbitrary nesting would be easier.
So, what sort of solution? I'm just going to sketch something out here, rather than write all the code - I'd be bound to make some silly mistake somewhere, 'cos right now my brain isn't parsing PHP to well. It uses an idea I've posted a couple of times already, in threads 10219330 and 10221160.
You find each occurrence of {b}...{/b} in the string (using {} instead of [] that does't contain any intervening {b} or {/b} tags.
Capture the bit in between, and replace it with some identifier. Store the captured bit along with the identifier in an array.
Keep doing this until there are no {b}...{/b} pairs found (this is the loop that regular expressions can't do) - in other words, the string doesn't change after the preg_replace(), so it means hanging on to the previous value of the string for a "before-and-after" comparison. The only {b} and {/b} tags left are unmatched ones.
Now go through the array you've built up. Do this in reverse order, 'cos some of the later captured bits might contain the identifiers from earlier replacements (cos of nesting). Replace identifiers in the string with <b>corresponding captured bit</b>.
By adding a third field to the array - "tagtype" to go alongside "captured text" and "identifier", you could proceed to match {i}...{/i} and other pairs of tags at the same time as you match and replace {b}...{/b} pairs.