WasmStructLayout.cpp (17623B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "wasm/WasmStructLayout.h" 8 9 #include "mozilla/DebugOnly.h" 10 #include "mozilla/HashFunctions.h" 11 12 #include "jstypes.h" // RoundUp 13 14 // This is a simple implementation of a layouter. It places the OOL pointer 15 // at the start of the IL payload area, regardless of whether an OOL area is 16 // actually necessary. 17 18 namespace js::wasm { 19 20 //========================================================================= 21 // BitVector 22 23 // See comment in WasmStructLayout.h for meaning of "byte", "offset" and 24 // "chunk". 25 26 #ifdef DEBUG 27 static bool Is8Aligned(uint32_t n) { return (n & 7) == 0; } 28 29 static bool IsWordAligned(uintptr_t x) { return (x % sizeof(void*)) == 0; } 30 #endif 31 32 static uint32_t IndexOfLeastSignificantZeroBit(uint8_t n) { 33 for (uint32_t i = 0; i < 8; i++) { 34 if (((n >> i) & 1) == 0) { 35 return i; 36 } 37 } 38 MOZ_CRASH(); 39 } 40 static uint32_t IndexOfLeastSignificantZero2Bits(uint8_t n) { 41 for (uint32_t i = 0; i < 8; i += 2) { 42 if (((n >> i) & 3) == 0) { 43 return i; 44 } 45 } 46 MOZ_CRASH(); 47 } 48 static uint32_t IndexOfLeastSignificantZero4Bits(uint8_t n) { 49 for (uint32_t i = 0; i < 8; i += 4) { 50 if (((n >> i) & 0xF) == 0) { 51 return i; 52 } 53 } 54 MOZ_CRASH(); 55 } 56 57 static uint32_t IndexOfMostSignificantOneBit(uint8_t n) { 58 for (int32_t i = 7; i >= 0; i--) { 59 if (((n >> i) & 1) == 1) { 60 return uint32_t(i); 61 } 62 } 63 MOZ_CRASH(); 64 } 65 66 #ifdef DEBUG 67 static uint32_t OffsetToChunkNumber(uint32_t offset) { return offset / 8; } 68 #endif 69 70 uint32_t BitVector::hashNonZero() const { 71 mozilla::HashNumber hash(42); 72 for (uint8_t b : chunks_) { 73 if (b != 0) { 74 hash = mozilla::AddToHash(hash, b); 75 } 76 } 77 return uint32_t(hash); 78 } 79 80 uint32_t BitVector::totalOffset() const { 81 if (chunks_.empty()) { 82 return 0; 83 } 84 // Find the highest non-zero chunk. 85 size_t i; 86 for (i = chunks_.length(); i >= 1; i--) { 87 if (chunks_[i - 1] != 0) { 88 break; 89 } 90 } 91 if (i == 0) { 92 // There are chunks, but none got used. 93 return 0; 94 } 95 i--; 96 MOZ_ASSERT(i < chunks_.length()); 97 return 8 * uint32_t(i) + IndexOfMostSignificantOneBit(chunks_[i]) + 1; 98 } 99 100 BitVector::Result BitVector::addMoreChunks() { 101 for (uint32_t i = 0; i < LookbackLimit / 2; i++) { 102 if (!chunks_.append(0)) { 103 return Result::OOM; 104 } 105 } 106 return Result::OK; 107 } 108 109 BitVector::Result BitVector::init(uint32_t chunksReserved, 110 uint32_t chunksTotal) { 111 MOZ_ASSERT_IF(chunksReserved > 0, chunksReserved < chunksTotal); 112 if (!chunks_.resize(chunksTotal)) { 113 return Result::OOM; 114 } 115 for (uint32_t i = 0; i < chunksReserved; i++) { 116 chunks_[i] = 0xFF; 117 } 118 for (uint32_t i = chunksReserved; i < chunksTotal; i++) { 119 chunks_[i] = 0; 120 } 121 return Result::OK; 122 } 123 124 BitVector::Result BitVector::allocate(uint32_t size, uint32_t firstChunk, 125 uint32_t lastChunkPlus1, 126 uint32_t* offset) { 127 MOZ_ASSERT(firstChunk < lastChunkPlus1); 128 MOZ_ASSERT(lastChunkPlus1 <= chunks_.length()); 129 130 // We don't want to re-scan the entire vector every search; that's 131 // expensive (quadratic). Instead just re-scan the last 24 chunks and 132 // accept that we'll miss out on the opportunity to use alignment holes 133 // more than 192 bytes back from the current "fill point" for the struct. 134 if (lastChunkPlus1 - firstChunk > LookbackLimit) { 135 firstChunk = lastChunkPlus1 - LookbackLimit; 136 } 137 138 // These are arranged in order of conceptually-simplest first. 139 switch (size) { 140 case 8: { 141 // Any chunk that is zero will do. 142 for (uint32_t i = firstChunk; i < lastChunkPlus1; i++) { 143 if (chunks_[i] == 0) { 144 *offset = i * 8; 145 chunks_[i] = 0xFF; 146 return Result::OK; 147 } 148 } 149 break; 150 } 151 case 16: { 152 // Any chunk-pair that is zero will do. Note this 8-aligns 16-byte 153 // requests, but we can't avoid that because the underlying JS heap 154 // allocator only provides 8-aligned addresses anyway. 155 for (uint32_t i = firstChunk + 1; i < lastChunkPlus1; i++) { 156 if (chunks_[i - 1] == 0 && chunks_[i] == 0) { 157 *offset = (i - 1) * 8; 158 chunks_[i - 1] = 0xFF; 159 chunks_[i] = 0xFF; 160 return Result::OK; 161 } 162 } 163 break; 164 } 165 // The 4, 2 and 1-byte cases are the most complex. We have to find a 166 // single chunk with that many consecutive, aligned bits, as zero. 167 case 1: { 168 // Any chunk that has an unset bit is fine. 169 for (uint32_t i = firstChunk; i < lastChunkPlus1; i++) { 170 if (chunks_[i] != 0xFF) { 171 uint32_t bitShift = IndexOfLeastSignificantZeroBit(chunks_[i]); 172 *offset = i * 8 + bitShift; 173 chunks_[i] |= (1 << bitShift); 174 return Result::OK; 175 } 176 } 177 break; 178 } 179 case 4: { 180 // Find a chunk in which either the upper or lower half is zero. 181 for (uint32_t i = firstChunk; i < lastChunkPlus1; i++) { 182 if ((chunks_[i] & (0xF << 0)) == 0 || (chunks_[i] & (0xF << 4)) == 0) { 183 uint32_t bitShift = IndexOfLeastSignificantZero4Bits(chunks_[i]); 184 *offset = i * 8 + bitShift; 185 chunks_[i] |= (0x0F << bitShift); 186 return Result::OK; 187 } 188 } 189 break; 190 } 191 case 2: { 192 // Find a chunk in which an adjacent bit-pair is zero. 193 for (uint32_t i = firstChunk; i < lastChunkPlus1; i++) { 194 if ((chunks_[i] & (3 << 0)) == 0 || (chunks_[i] & (3 << 2)) == 0 || 195 (chunks_[i] & (3 << 4)) == 0 || (chunks_[i] & (3 << 6)) == 0) { 196 uint32_t bitShift = IndexOfLeastSignificantZero2Bits(chunks_[i]); 197 *offset = i * 8 + bitShift; 198 chunks_[i] |= (3 << bitShift); 199 return Result::OK; 200 } 201 } 202 break; 203 } 204 default: { 205 MOZ_CRASH(); 206 } 207 } 208 return Result::Fail; 209 } 210 211 // Given that `offset` was allocated by a call to `allocate` 212 // (requesting size `size`), free up that area. 213 void BitVector::deallocate(uint32_t offset, uint32_t size) { 214 MOZ_ASSERT(OffsetToChunkNumber(offset + size - 1) < chunks_.length()); 215 switch (size) { 216 case 8: { 217 MOZ_ASSERT((offset % 8) == 0); 218 uint32_t chunk = offset / 8; 219 MOZ_ASSERT(chunks_[chunk] == 0xFF); 220 chunks_[chunk] = 0; 221 break; 222 } 223 case 16: { 224 MOZ_ASSERT((offset % 8) == 0); // re 8, see comment on ::allocate 225 uint32_t chunk = offset / 8; 226 MOZ_ASSERT(chunk + 1 < chunks_.length()); 227 MOZ_ASSERT(chunks_[chunk] == 0xFF); 228 MOZ_ASSERT(chunks_[chunk + 1] == 0xFF); 229 chunks_[chunk] = 0; 230 chunks_[chunk + 1] = 0; 231 break; 232 } 233 case 1: { 234 uint32_t chunk = offset / 8; 235 uint32_t shift = offset % 8; // 0, 1, 2, 3, 4, 5, 6 or 7 236 uint8_t mask = 1 << shift; 237 MOZ_ASSERT((chunks_[chunk] & mask) == mask); 238 chunks_[chunk] &= ~mask; 239 break; 240 } 241 case 4: { 242 MOZ_ASSERT((offset % 4) == 0); 243 uint32_t chunk = offset / 8; 244 uint32_t shift = offset % 8; // 0 or 4 245 uint8_t mask = 0xF << shift; 246 MOZ_ASSERT((chunks_[chunk] & mask) == mask); 247 chunks_[chunk] &= ~mask; 248 break; 249 } 250 case 2: { 251 MOZ_ASSERT((offset % 2) == 0); 252 uint32_t chunk = offset / 8; 253 uint32_t shift = offset % 8; // 0, 2, 4 or 6 254 uint8_t mask = 0x3 << shift; 255 MOZ_ASSERT((chunks_[chunk] & mask) == mask); 256 chunks_[chunk] &= ~mask; 257 break; 258 } 259 default: { 260 MOZ_CRASH(); 261 } 262 } 263 } 264 265 //========================================================================= 266 // FixedSizeBitVector 267 268 BitVector::Result FixedSizeBitVector::init(uint32_t layoutBytesReserved, 269 uint32_t layoutBytesTotal) { 270 MOZ_ASSERT(layoutBytesTotal > 0); 271 MOZ_ASSERT(layoutBytesReserved < layoutBytesTotal); 272 MOZ_ASSERT(Is8Aligned(layoutBytesReserved)); 273 MOZ_ASSERT(Is8Aligned(layoutBytesTotal)); 274 chunksReserved_ = layoutBytesReserved / 8; 275 chunksTotal_ = layoutBytesTotal / 8; 276 return BitVector::init(chunksReserved_, chunksTotal_); 277 } 278 279 BitVector::Result FixedSizeBitVector::allocate(uint32_t size, 280 uint32_t* offset) { 281 return BitVector::allocate(size, chunksReserved_, chunksTotal_, offset); 282 } 283 284 //========================================================================= 285 // VariableSizeBitVector 286 287 BitVector::Result VariableSizeBitVector::init() { 288 // This initial size of 1 is important in that it needs to be less than 289 // ::LookbackLimit. 290 return BitVector::init(0, 1 /*see ::unused()*/); 291 } 292 293 BitVector::Result VariableSizeBitVector::allocate(uint32_t size, 294 uint32_t* offset) { 295 // First, try to find it given the chunks we already have. 296 Result res = BitVector::allocate(size, 0, chunks_.length(), offset); 297 if (res == Result::OOM) { 298 return Result::OOM; 299 } 300 if (res == Result::OK) { 301 used_ = true; 302 return res; 303 } 304 // That failed, so add some more (uncommitted) chunks on the end of `chunks_` 305 // and try again. This second attempt *must* succeed since we can make the 306 // OOL block arbitrarily large. 307 res = addMoreChunks(); 308 if (res == Result::OOM) { 309 return Result::OOM; 310 } 311 res = BitVector::allocate(size, 0, chunks_.length(), offset); 312 if (res == Result::OOM) { 313 return Result::OOM; 314 } 315 MOZ_RELEASE_ASSERT(res == Result::OK); 316 used_ = true; 317 return Result::OK; 318 } 319 320 bool VariableSizeBitVector::unused() const { return !used_; } 321 322 uint32_t VariableSizeBitVector::totalOffset() const { 323 uint32_t res = BitVector::totalOffset(); 324 MOZ_ASSERT(used_ == (res > 0)); 325 return res; 326 } 327 328 //========================================================================= 329 // StructLayout 330 331 bool StructLayout::init(uint32_t firstUsableILOffset, uint32_t usableILSize) { 332 // Not actually necessary, but it would be strange if this wasn't so. 333 MOZ_ASSERT(IsWordAligned(firstUsableILOffset)); 334 MOZ_ASSERT(IsWordAligned(usableILSize)); 335 // Must have at least enough space to hold the OOL pointer 336 MOZ_ASSERT(usableILSize >= sizeof(void*)); 337 oolptrILO_ = InvalidOffset; 338 BitVector::Result res = ilBitVector_.init(firstUsableILOffset, 339 firstUsableILOffset + usableILSize); 340 if (res == BitVector::Result::OOM) { 341 return false; 342 } 343 res = oolBitVector_.init(); 344 if (res == BitVector::Result::OOM) { 345 return false; 346 } 347 return true; 348 } 349 350 // Add a field of the specified size, and get back its access path. The two 351 // release assertions together guarantee that the maximum offset that could be 352 // generated is roughly `16 * js::wasm::MaxStructFields`, so there is no need 353 // to use checked integers in the layout computations. 354 355 bool StructLayout::addField(uint32_t fieldSize, FieldAccessPath* path) { 356 MOZ_ASSERT(fieldSize == 16 || fieldSize == 8 || fieldSize == 4 || 357 fieldSize == 2 || fieldSize == 1); 358 // Guard against field-offset overflow. 359 numFieldsProcessed_++; 360 MOZ_RELEASE_ASSERT(numFieldsProcessed_ <= js::wasm::MaxStructFields); 361 MOZ_RELEASE_ASSERT(fieldSize <= 16); 362 363 *path = FieldAccessPath(); 364 365 // This is complex. In between calls to ::addField, we maintain the 366 // following invariant: 367 // 368 // (0) If the OOL area is not in use, then it is possible to allocate the OOL 369 // pointer in the IL area. 370 // 371 // With that in place, the code below deals with 4 cases: 372 // 373 // (1) The OOL area is unused, and both the field and a dummy OOL pointer fit 374 // into the IL area. Allocate the field IL and leave the OOL area 375 // unused. Because we just established that a dummy OOL pointer fits in 376 // IL after the field, and because of (N) below, (0) is true after the 377 // call. 378 // 379 // (2) The OOL area is unused, but the field and dummy OOL pointer don't both 380 // fit in the IL area. We need to bring the OOL area into use. Allocate 381 // the OOL pointer in the IL area (which due to (0) cannot fail), and 382 // allocate the field in the OOL area. This is a one-time transitional 383 // case that separates multiple occurrences of (1) from multiple 384 // occurrences of (3) and (4). This means the OOL area is now in use, so 385 // (0) is trivially true after the call. 386 // 387 // (3) The OOL area is in use, but the field fits in the IL area anyways, 388 // presumably because it falls into an alignment hole in the IL area. 389 // Just allocate it IL and leave everything else unchanged. Since the 390 // OOL area is in use, (0) is trivially true after the call. 391 // 392 // (4) The OOL area is in use, and the field doesn't fit in the IL area. 393 // Allocate it in the OOL area. Since the OOL area is in use, (0) is 394 // trivially true after the call. 395 // 396 // (N) Note: for (1) and (2) it is important to try for the allocation of the 397 // field first and the dummy OOL pointer second. 398 399 // For cases (1) and (2) we need to back out tentative allocations. Hash the 400 // current state so we can later assert it is unchanged after backouts. 401 mozilla::DebugOnly<uint32_t> initialHash = hash(); 402 403 // These need to agree. 404 MOZ_ASSERT(oolBitVector_.unused() == (oolptrILO_ == InvalidOffset)); 405 406 // Try for Case (1) 407 if (oolBitVector_.unused()) { 408 uint32_t fieldOffset = InvalidOffset; 409 BitVector::Result res = ilBitVector_.allocate(fieldSize, &fieldOffset); 410 if (res == BitVector::Result::OOM) { 411 return false; 412 } 413 // The field fits, now try for the dummy OOL pointer 414 mozilla::DebugOnly<uint32_t> hash2 = hash(); 415 if (res == BitVector::Result::OK) { 416 uint32_t dummyOffset = InvalidOffset; 417 res = ilBitVector_.allocate(sizeof(void*), &dummyOffset); 418 if (res == BitVector::Result::OOM) { 419 return false; 420 } 421 if (res == BitVector::Result::OK) { 422 // Case (1) established -- they both fit. 423 // Back out the dummy OOL pointer allocation, and we're done. 424 MOZ_ASSERT(fieldOffset != dummyOffset); 425 ilBitVector_.deallocate(dummyOffset, sizeof(void*)); 426 MOZ_ASSERT(hash() == hash2); 427 *path = FieldAccessPath(fieldOffset); 428 return true; 429 } 430 // The field fits, but the OOL pointer doesn't. Back out the field 431 // allocation, so that we have changed nothing. 432 ilBitVector_.deallocate(fieldOffset, fieldSize); 433 } 434 } 435 436 // "state is unchanged from when we started" 437 MOZ_ASSERT(hash() == initialHash); 438 MOZ_ASSERT(oolBitVector_.unused() == (oolptrILO_ == InvalidOffset)); 439 440 // Try for Case (2) 441 if (oolBitVector_.unused()) { 442 // We need to bring the OOL area into use. First, try to allocate the OOL 443 // pointer field. This must succeed (apart from OOMing) because of (1). 444 uint32_t oolptrOffset = InvalidOffset; 445 BitVector::Result res = ilBitVector_.allocate(sizeof(void*), &oolptrOffset); 446 if (res == BitVector::Result::OOM) { 447 return false; 448 } 449 MOZ_ASSERT(res == BitVector::Result::OK); 450 // Case (2) established 451 oolptrILO_ = oolptrOffset; 452 // Allocate the field in the OOL area; it is the first item there. 453 uint32_t fieldOffset = InvalidOffset; 454 res = oolBitVector_.allocate(fieldSize, &fieldOffset); 455 if (res == BitVector::Result::OOM) { 456 return false; 457 } 458 // Allocation in the OOL area can't fail. 459 MOZ_RELEASE_ASSERT(res == BitVector::Result::OK); 460 MOZ_ASSERT(!oolBitVector_.unused()); 461 // We expect this because this is the first item in the OOL area. 462 MOZ_ASSERT(fieldOffset == 0); 463 *path = FieldAccessPath(oolptrILO_, fieldOffset); 464 return true; 465 } 466 467 // "state is unchanged from when we started" 468 MOZ_ASSERT(hash() == initialHash); 469 MOZ_ASSERT(!oolBitVector_.unused() && oolptrILO_ != InvalidOffset); 470 471 // Cases (3) and (4). In both cases, the OOL area is in use. 472 // Re-try allocating the field IL. Note this is not redundant w.r.t. the 473 // logic above, since that involved trying to allocate both the field and 474 // the dummy OOL pointer; this only tries to allocate the field. 475 uint32_t fieldOffset = InvalidOffset; 476 BitVector::Result res = ilBitVector_.allocate(fieldSize, &fieldOffset); 477 if (res == BitVector::Result::OOM) { 478 return false; 479 } 480 if (res == BitVector::Result::OK) { 481 // Case (3) established 482 *path = FieldAccessPath(fieldOffset); 483 return true; 484 } 485 // Case (4) established 486 fieldOffset = InvalidOffset; 487 res = oolBitVector_.allocate(fieldSize, &fieldOffset); 488 if (res == BitVector::Result::OOM) { 489 return false; 490 } 491 // Allocation in the OOL area can't fail. 492 MOZ_RELEASE_ASSERT(res == BitVector::Result::OK); 493 *path = FieldAccessPath(oolptrILO_, fieldOffset); 494 return true; 495 } 496 497 uint32_t StructLayout::hash() const { 498 uint32_t h = ilBitVector_.hashNonZero(); 499 h = (h << 16) | (h >> 16); 500 h ^= oolBitVector_.hashNonZero(); 501 return h; 502 } 503 504 uint32_t StructLayout::totalSizeIL() const { 505 return js::RoundUp(ilBitVector_.totalOffset(), sizeof(void*)); 506 } 507 508 bool StructLayout::hasOOL() const { return !oolBitVector_.unused(); } 509 510 uint32_t StructLayout::totalSizeOOL() const { 511 MOZ_ASSERT(hasOOL()); 512 MOZ_ASSERT(oolptrILO_ != InvalidOffset); 513 return js::RoundUp(oolBitVector_.totalOffset(), sizeof(void*)); 514 } 515 516 FieldAccessPath StructLayout::oolPointerPath() const { 517 MOZ_ASSERT(hasOOL()); 518 MOZ_ASSERT(oolptrILO_ != InvalidOffset); 519 return FieldAccessPath(oolptrILO_); 520 } 521 522 } // namespace js::wasm