I just stumbled upon the same problem. For me, using my own statement class (extending PDOStatement) with my own execute() method fixed it.
This is the class:
class MyPDOStatement extends PDOStatement {
  public function execute($input_parameters = null) {
    if (is_array($input_parameters)) {
      $i = 1;
      foreach ($input_parameters as $p) {
        // default to string datatype
        $parameterType = PDO::PARAM_STR;
        // now let's see if there is something more appropriate
        if (is_bool($p)) {
          $parameterType = PDO::PARAM_BOOL;
        } elseif (is_null($p)) {
          $parameterType = PDO::PARAM_NULL;
        } elseif (is_int($p)) {
          $parameterType = PDO::PARAM_INT;
        }
        // parameters passed to execute() are input-only parameters, so use
        // bindValue()
        $this->bindValue($i, $p, $parameterType);
        $i++;
      }
    }
    return parent::execute();
  }
}
To tell PDO to use this statement class instead of the default one, do this:
$db = new PDO(...);
$db->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('MyPDOStatement'));
Now the code in the question will work:
$start = 0;
$rows = 20;
$sql = "SELECT * FROM tbl_news ORDER BY date DESC LIMIT ?, ?";
$q = $db->prepare($sql);
$q->execute(array($start , $rows));
The only thing you have to make shure is that the variables bound to the statement have the correct type, integer. If you have a numeric string, e.g. from the $_GET array, you can do something like this:
if (isset($_GET['start']) && is_numeric($_GET['start'])
    && is_int($_GET['start'] + 0) {
  $start = (int) $_GET['start'];
}
I'm not shure if there is an easier way for the last thing, but at least it works fine for me.