The difference between trim_start and trim_end is that the first pass of the loop in trim_start never matches while it always matches in trim_end. Change your input to ' abc ,def' and you'll see the same problem in both trim_start and trim_end.
On a successful match, $1 and $2 are cleared and so are $_[0] and $_[1] (since they are aliased to $1 and $2). On an unsuccessful match, $1 and $2 are left untouched.
Passing a global as an argument is bad, especially when that global is changed by the function to which it is being passed. The best solution is to pass a copy of the global to the function. This can be done by simply changing the call to
$sub->("$1", "$2");
Update: Added explanation.