Bạn có thể làm gì trong MSIL mà bạn không thể làm trong C # hoặc VB.NET? [đóng cửa]


165

Tất cả các mã được viết bằng ngôn ngữ .NET biên dịch thành MSIL, nhưng có các tác vụ / thao tác cụ thể mà bạn chỉ có thể thực hiện khi sử dụng MSIL trực tiếp không?

Chúng ta cũng có những thứ được thực hiện dễ dàng hơn trong MSIL so với C #, VB.NET, F #, j # hoặc bất kỳ ngôn ngữ .NET nào khác.

Cho đến nay chúng ta có điều này:

  1. Đuôi đệ quy
  2. Chung chung / Chống chỉ định
  3. Quá tải chỉ khác nhau ở các loại trả về
  4. Ghi đè sửa đổi truy cập
  5. Có một lớp không thể kế thừa từ System.Object
  6. Các ngoại lệ được lọc (có thể được thực hiện trong vb.net)
  7. Gọi một phương thức ảo của loại lớp tĩnh hiện tại.
  8. Nhận một điều khiển trên phiên bản đóng hộp của một loại giá trị.
  9. Làm thử / lỗi.
  10. Sử dụng tên cấm.
  11. Xác định các hàm tạo không tham số của riêng bạn cho các loại giá trị .
  12. Xác định các sự kiện với một raiseyếu tố.
  13. Một số chuyển đổi được CLR cho phép nhưng không phải bởi C #.
  14. Thực hiện một main()phương pháp không phải là .entrypoint.
  15. làm việc với các loại bản địa intvà bản địa unsigned inttrực tiếp.
  16. Chơi với con trỏ thoáng qua
  17. chỉ thị phát ra trong Phương thứcBodyItem
  18. Ném và bắt các loại System.Exception
  19. Kế thừa Enums (Chưa được xác minh)
  20. Bạn có thể coi một mảng byte là một mảng int (nhỏ hơn 4 lần).
  21. Bạn có thể có một trường / phương thức / thuộc tính / sự kiện đều có cùng tên (Chưa được xác minh).
  22. Bạn có thể phân nhánh trở lại thành một khối thử từ khối bắt riêng của nó.
  23. Bạn có quyền truy cập vào công cụ xác định truy cập famandassem ( protected internallà fam hoặc lắp ráp)
  24. Truy cập trực tiếp vào <Module>lớp để xác định các hàm toàn cục hoặc trình khởi tạo mô-đun.

17
Câu hỏi tuyệt vời!
Tamas Czinege

5
F # không hỗ trợ đệ quy đuôi xem: en.wikibooks.org/wiki/F_Sharp_Programming/Recursion
Bas Bossink 13/03/2016

4
Kế thừa enum? Điều đó đôi khi thật tuyệt vời ..
Jimmy Hoffa

1
Phương thức chính có vốn M trong .NET
Concrete Gannet

4
Yêu cầu "đóng như không mang tính xây dựng" là vô lý. Đây là một câu hỏi thực nghiệm.
Jim Balter

Câu trả lời:


34

MSIL cho phép quá tải chỉ khác nhau ở các loại trả về vì

call void [mscorlib]System.Console::Write(string)

hoặc là

callvirt int32 ...

5
Làm thế nào để bạn biết loại công cụ này? :)
Gerrie Schenck


8
Điều này thật tuyệt. Ai đã không muốn làm quá tải trở lại?
Jimmy Hoffa

12
Nếu hai phương thức giống hệt nhau ngoại trừ kiểu trả về, có thể được gọi từ C # hoặc vb.net không?
supercat

29

Hầu hết các ngôn ngữ .Net bao gồm C # và VB không sử dụng tính năng đệ quy đuôi của mã MSIL.

Đệ quy đuôi là một tối ưu hóa phổ biến trong các ngôn ngữ chức năng. Nó xảy ra khi một phương thức A kết thúc bằng cách trả về giá trị của phương thức B sao cho ngăn xếp của phương thức A có thể được giải quyết sau khi lệnh gọi phương thức B được thực hiện.

Mã MSIL hỗ trợ đệ quy đuôi một cách rõ ràng và đối với một số thuật toán, đây có thể là một tối ưu hóa quan trọng để thực hiện. Nhưng vì C # và VB không tạo ra các hướng dẫn để làm điều này, nên nó phải được thực hiện thủ công (hoặc sử dụng F # hoặc một số ngôn ngữ khác).

Dưới đây là một ví dụ về cách đệ quy đuôi có thể được thực hiện thủ công trong C #:

private static int RecursiveMethod(int myParameter)
{
    // Body of recursive method
    if (BaseCase(details))
        return result;
    // ...

    return RecursiveMethod(modifiedParameter);
}

// Is transformed into:

private static int RecursiveMethod(int myParameter)
{
    while (true)
    {
        // Body of recursive method
        if (BaseCase(details))
            return result;
        // ...

        myParameter = modifiedParameter;
    }
}

Đó là thực tế phổ biến để loại bỏ đệ quy bằng cách di chuyển dữ liệu cục bộ từ ngăn xếp phần cứng lên cấu trúc dữ liệu ngăn xếp được phân bổ heap. Trong loại bỏ đệ quy cuộc gọi đuôi như hình trên, ngăn xếp được loại bỏ hoàn toàn, đó là một tối ưu hóa khá tốt. Ngoài ra, giá trị trả về không phải đi lên chuỗi cuộc gọi dài mà được trả lại trực tiếp.

Nhưng, dù sao, CIL cung cấp tính năng này như một phần của ngôn ngữ, nhưng với C # hoặc VB, nó phải được thực hiện thủ công. (Jitter cũng được tự do thực hiện tối ưu hóa này, nhưng đó là một vấn đề hoàn toàn khác.)


1
F # không sử dụng đệ quy đuôi của MSIL, vì nó chỉ hoạt động trong các trường hợp (CAS) hoàn toàn đáng tin cậy do cách nó không rời khỏi ngăn xếp để kiểm tra các xác nhận cấp phép (v.v.).
Richard

10
Richard, tôi không chắc ý của bạn là gì. F # chắc chắn không phát ra đuôi. gọi tiền tố, khá nhiều ở khắp mọi nơi. Kiểm tra IL cho điều này: "hãy in x = print_any x".
MichaelGG

1
Tôi tin rằng JIT sẽ sử dụng đệ quy đuôi trong mọi trường hợp - và trong một số trường hợp sẽ bỏ qua yêu cầu rõ ràng cho nó. Nó phụ thuộc vào kiến ​​trúc bộ xử lý, IIRC.
Jon Skeet

3
@Abel: Mặc dù về mặt lý thuyết , kiến trúc bộ xử lý không liên quan , nhưng thực tế không liên quan vì các JIT khác nhau cho các kiến ​​trúc khác nhau có các quy tắc khác nhau cho đệ quy đuôi trong .NET. Nói cách khác, bạn rất dễ dàng có một chương trình xuất hiện trên x86 nhưng không phải trên x64. Chỉ vì đuôi đệ quy có thể được thực hiện trong cả hai trường hợp không có nghĩa là nó . Lưu ý rằng câu hỏi này là về .NET cụ thể.
Jon Skeet

2
C # thực sự thực hiện các cuộc gọi đuôi dưới x64 trong các trường hợp cụ thể: Community.bartdesmet.net/bloss/bart/archive/2010/07/07/ .
Pieter van Ginkel

21

Trong MSIL, bạn có thể có một lớp không thể kế thừa từ System.Object.

Mã mẫu: biên dịch nó với ilasm.exe CẬP NHẬT: Bạn phải sử dụng "/ NOAUTOINHERIT" để ngăn trình biên dịch tự động kế thừa.

// Metadata version: v2.0.50215
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 2:0:0:0
}
.assembly sample
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
  .hash algorithm 0x00008004
  .ver 0:0:0:0
}
.module sample.exe
// MVID: {A224F460-A049-4A03-9E71-80A36DBBBCD3}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x02F20000


// =============== CLASS MEMBERS DECLARATION ===================

.class public auto ansi beforefieldinit Hello
{
  .method public hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // Code size       13 (0xd)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "Hello World!"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method Hello::Main
} // end of class Hello

2
@Jon Skeet - Với tất cả sự tôn trọng, bạn có thể vui lòng giúp tôi hiểu NOAUTOINHERIT nghĩa là gì không. MSDN chỉ định "Tắt tính kế thừa mặc định khỏi Object khi không có lớp cơ sở nào được chỉ định. Mới trong .NET Framework phiên bản 2.0."
Ramesh

2
@Michael - Câu hỏi liên quan đến MSIL và không phải ngôn ngữ trung gian phổ biến. Tôi đồng ý điều này có thể không khả thi trong CIL nhưng, nó vẫn hoạt động với MSIL
Ramesh

4
@Ramesh: Rất tiếc, bạn hoàn toàn đúng. Tôi đã nói rằng tại thời điểm đó, nó phá vỡ thông số kỹ thuật tiêu chuẩn và không nên được sử dụng. Reflector thậm chí không tải assmebly. Tuy nhiên, nó có thể được thực hiện với ilasm. Tôi tự hỏi tại sao trên trái đất nó ở đó.
Jon Skeet

4
(Ah, tôi thấy / chút noautoinherit đã được bổ sung sau khi nhận xét của tôi Ít nhất tôi cảm thấy hơi tốt hơn về việc không nhận ra nó trước khi ....)
Jon Skeet

1
Tôi sẽ thêm rằng ít nhất là trên .NET 4.5.2 trên Windows, nó biên dịch nhưng không thực thi ( TypeLoadException). Trả về PEVerify: [MD]: Lỗi: TypeDef không phải là Giao diện và không phải là lớp Object mở rộng mã thông báo Nil.
xanatos

20

Có thể kết hợp protectedinternalsửa đổi truy cập. Trong C #, nếu bạn viết protected internalmột thành viên có thể truy cập được từ hội đồng và từ các lớp dẫn xuất. Qua MSIL bạn có thể có được một thành viên có thể truy cập từ các lớp học có nguồn gốc trong lắp ráp chỉ . (Tôi nghĩ rằng nó có thể khá hữu ích!)


4
Đây là một ứng cử viên hiện đang được triển khai trong C # 7.1 ( github.com/dotnet/csharplang/issues/37 ), công cụ sửa đổi truy cập làprivate protected
Happypig375

5
Nó được phát hành như một phần của C # 7.2: blog.msdn.microsoft.com/dotnet/2017/11/15/iêu
Joe Sewell

18

Ồ, tôi đã không phát hiện ra điều này vào thời điểm đó. (Nếu bạn thêm thẻ jon-skeet thì nhiều khả năng, nhưng tôi không kiểm tra nó thường xuyên.)

Có vẻ như bạn đã có câu trả lời khá tốt rồi. Ngoài ra:

  • Bạn không thể xử lý phiên bản được đóng hộp của loại giá trị trong C #. Bạn có thể trong C ++ / CLI
  • Bạn không thể thực hiện thử / lỗi trong C # ("lỗi" giống như "bắt tất cả mọi thứ và suy nghĩ lại ở cuối khối" hoặc "cuối cùng nhưng chỉ khi thất bại")
  • Có rất nhiều tên bị cấm bởi C # nhưng IL hợp pháp
  • IL cho phép bạn xác định các hàm tạo không tham số của riêng bạn cho các loại giá trị .
  • Bạn không thể xác định các sự kiện có yếu tố "nâng cao" trong C #. (Trong VB bạn phải cho các sự kiện tùy chỉnh, nhưng các sự kiện "mặc định" không bao gồm một sự kiện.)
  • Một số chuyển đổi được CLR cho phép nhưng không phải bởi C #. Nếu bạn đi qua objecttrong C #, đôi khi chúng sẽ hoạt động. Xem một câu hỏi uint [] / int [] SO để biết ví dụ.

Tôi sẽ thêm vào điều này nếu tôi nghĩ về bất cứ điều gì khác ...


3
Ah thẻ jon-skeet, tôi biết tôi đang thiếu thứ gì đó!
Binoj Antony

Để sử dụng tên định danh bất hợp pháp, bạn có thể đặt tiền tố với @ in C #
George Polevoy

4
@George: Điều đó hoạt động cho các từ khóa, nhưng không phải tất cả các tên IL hợp lệ. Hãy thử chỉ định <>atên trong C # ...
Jon Skeet


14

Trong IL bạn có thể ném và bắt bất kỳ loại nào, không chỉ các loại có nguồn gốc từ System.Exception.


6
Bạn cũng có thể làm điều đó trong C #, với try/ catchkhông có dấu ngoặc đơn trong câu lệnh bắt, bạn cũng sẽ bắt gặp các ngoại lệ không giống ngoại lệ. Ném, tuy nhiên, thực sự chỉ có thể khi bạn thừa kế từ Exception.
Abel

@Abel Bạn khó có thể nói rằng bạn đang bắt một cái gì đó nếu bạn không thể tham khảo nó.
Jim Balter

2
@JimBalter nếu bạn không bắt được nó, ứng dụng gặp sự cố. Nếu bạn bắt được nó, ứng dụng không bị sập. Vì vậy, đề cập đến đối tượng ngoại lệ là khác biệt với việc bắt nó.
Daniel Earwicker

Cười lớn! Một sự khác biệt giữa chấm dứt hoặc tiếp tục ứng dụng là sư phạm? Bây giờ tôi nghĩ rằng tôi có thể đã nghe thấy tất cả mọi thứ.
Daniel Earwicker

Thật thú vị, CLR không còn cho phép bạn làm điều này. Theo mặc định, nó sẽ bao bọc các đối tượng không ngoại lệ trong RuntimeWrappingException .
Jwosty

10

IL có sự phân biệt giữa callcallvirtcho các cuộc gọi phương thức ảo. Bằng cách sử dụng trước đây, bạn có thể buộc gọi một phương thức ảo của loại lớp tĩnh hiện tại thay vì hàm ảo trong loại lớp động.

C # không có cách nào để làm điều này:

abstract class Foo {
    public void F() {
        Console.WriteLine(ToString()); // Always a virtual call!
    }

    public override string ToString() { System.Diagnostics.Debug.Assert(false); }
};

sealed class Bar : Foo {
    public override string ToString() { return "I'm called!"; }
}

VB, giống như IL, có thể thực hiện các cuộc gọi không ảo bằng cách sử dụng MyClass.Method()cú pháp. Ở trên, điều này sẽ được MyClass.ToString().


9

Trong thử / bắt, bạn có thể nhập lại khối thử từ khối bắt của chính nó. Vì vậy, bạn có thể làm điều này:

.try {
    // ...

  MidTry:
    // ...

    leave.s RestOfMethod
}
catch [mscorlib]System.Exception {
    leave.s MidTry  // branching back into try block!
}

RestOfMethod:
    // ...

AFAIK bạn không thể làm điều này trong C # hoặc VB


3
Tôi có thể thấy lý do tại sao điều này bị bỏ qua - Nó có mùi riêng biệtGOTO
Cơ bản

3
Nghe có vẻ giống như On Error Resume Next trong VB.NET
Thomas Weller

1
Điều này thực sự có thể được thực hiện trong VB.NET. Câu lệnh GoTo có thể phân nhánh từ CatchTry . Chạy mã kiểm tra trực tuyến tại đây .
mbomb007

9

Với IL và VB.NET, bạn có thể thêm các bộ lọc khi bắt ngoại lệ, nhưng C # v3 không hỗ trợ tính năng này.

Ví dụ VB.NET này được lấy từ http://bloss.msdn.com/clrteam/archive/2009/02/05/catch-rethrow-and-filters-why-you-should-care.aspx (lưu ý When ShouldCatch(ex) = Truetrong Điều khoản bắt):

Try
   Foo()
Catch ex As CustomBaseException When ShouldCatch(ex)
   Console.WriteLine("Caught exception!")
End Try

17
Làm ơn hãy nghĩ lại = True, nó làm tôi chảy máu mắt!
Konrad Rudolph

Tại sao? Đây là VB chứ không phải C #, vì vậy không tồn tại vấn đề = / ==. ;-)
peSHIr

Vâng c # có thể làm "ném;", vì vậy kết quả tương tự có thể đạt được.
Frank Schwieterman

8
paSHIr, tôi tin rằng anh ta đã nói về sự chuyển hướng của nó
LegendLpm

5
@Frank Schwieterman: Có một sự khác biệt giữa việc bắt và lấy lại một ngoại lệ, so với việc không nắm bắt nó. Bộ lọc chạy trước bất kỳ câu lệnh "cuối cùng" lồng nhau nào, do đó, trường hợp gây ra ngoại lệ sẽ vẫn tồn tại khi bộ lọc được chạy. Nếu một người đang mong đợi một số lượng đáng kể của SocketException sẽ bị ném mà người ta sẽ muốn bắt tương đối im lặng, nhưng một vài trong số họ sẽ báo hiệu sự cố, có thể kiểm tra trạng thái khi ném một vấn đề có thể rất hữu ích.
supercat


7

Native types
Bạn có thể làm việc trực tiếp với các kiểu int int và unsign gốc (trong c #, bạn chỉ có thể làm việc trên một IntPtr không giống nhau.

Transient Pointers
Bạn có thể chơi với các con trỏ thoáng qua, là các con trỏ tới các loại được quản lý nhưng đảm bảo không di chuyển trong bộ nhớ vì chúng không nằm trong heap được quản lý. Không hoàn toàn chắc chắn làm thế nào bạn có thể sử dụng điều này một cách hữu ích mà không gây rối với mã không được quản lý nhưng nó không được tiếp xúc trực tiếp với các ngôn ngữ khác chỉ thông qua những thứ như stackalloc.

<Module>
bạn có thể gây rối với lớp nếu bạn mong muốn (bạn có thể thực hiện điều này bằng cách phản chiếu mà không cần IL)

.emitbyte

15.4.1.1 Phương thức chỉ thị .emitbyteBodyItem :: = trên | .emitbyte Int32 Lệnh này làm cho giá trị 8 bit không dấu được phát trực tiếp vào luồng CIL của phương thức, tại điểm xuất hiện lệnh này. [Lưu ý: Lệnh .emitbyte được sử dụng để tạo các bài kiểm tra. Nó không phải là bắt buộc trong việc tạo ra các chương trình thông thường. lưu ý cuối]

.entrypoint
Bạn có một chút linh hoạt hơn về điều này, bạn có thể áp dụng nó cho các phương thức không được gọi là Main chẳng hạn.

đọc thông số kỹ thuật tôi chắc chắn bạn sẽ tìm thấy một vài chi tiết.


+1, một số đá quý ẩn ở đây. Lưu ý rằng đó <Module>là lớp đặc biệt cho các ngôn ngữ chấp nhận các phương thức toàn cầu (như VB hiện), nhưng thực tế, C # không thể truy cập trực tiếp vào nó.
Abel

Con trỏ thoáng qua có vẻ như chúng là một loại rất hữu ích; nhiều sự phản đối đối với các cấu trúc đột biến xuất phát từ sự thiếu sót của chúng. Ví dụ: "DictOfPoints (khóa) .X = 5;" sẽ khả thi nếu DictOfPoints (khóa) trả về một con trỏ thoáng qua cho một cấu trúc, thay vì sao chép cấu trúc theo giá trị.
supercat

@supercat sẽ không hoạt động với một con trỏ thoáng qua, dữ liệu được đề cập có thể nằm trên đống. những gì bạn muốn là giới thiệu trả lại các cuộc nói chuyện của Eric về đây: blog.msdn.com/b/ericlippert/archive/2011/06/23/ chủ
ShuggyCoUk

@ShuggyCoUk: Thú vị. Tôi thực sự hy vọng Eric có thể được thuyết phục để cung cấp một phương tiện theo đó "DictOfPoints (khóa) .X = 5;" có thể được thực hiện để làm việc. Ngay bây giờ nếu một người muốn mã hóa DictOfPoints hoạt động độc quyền với loại Điểm (hoặc một số loại cụ thể khác), người ta gần như có thể làm cho công việc đó hoạt động, nhưng đó là một nỗi đau. BTW, một điều mà tôi muốn thấy sẽ là một phương tiện để người ta có thể viết một hàm chung kết thúc mở, ví dụ DoStuff <...> (someParams, ActionByRef <moreParams, ...>, ...) có thể mở rộng khi cần thiết. Tôi đoán sẽ có một số cách để làm điều đó trong MSIL; với một số trợ giúp về trình biên dịch ...
supercat

@ShuggyCoUk: ... nó sẽ cung cấp một cách khác để có các thuộc tính tham chiếu, với phần thưởng bổ sung mà một số thuộc tính có thể chạy sau khi mọi thứ mà người ngoài sẽ làm đối với tham chiếu đã được thực hiện.
supercat

6

Bạn có thể hack phương thức ghi đè co / contra-variance, điều mà C # không cho phép (điều này KHÔNG giống với phương sai chung!). Tôi đã có thêm thông tin về việc thực hiện điều này ở đây , và phần 12


4

Tôi nghĩ rằng điều tôi luôn mong muốn (với những lý do hoàn toàn sai lầm) là sự kế thừa trong Enums. Nó dường như không phải là một điều khó làm trong SMIL (vì Enums chỉ là các lớp) nhưng đó không phải là điều mà cú pháp C # muốn bạn làm.


4

Dưới đây là một số:

  1. Bạn có thể có các phương thức ví dụ bổ sung trong các đại biểu.
  2. Các đại biểu có thể thực hiện các giao diện.
  3. Bạn có thể có các thành viên tĩnh trong các đại biểu và giao diện.

3

20) Bạn có thể coi một mảng byte là một mảng int (nhỏ hơn 4 lần).

Tôi đã sử dụng điều này gần đây để thực hiện XOR nhanh, vì hàm CLR xor hoạt động trên ints và tôi cần thực hiện XOR trên luồng byte.

Mã kết quả được đo là nhanh hơn ~ 10 lần so với mã tương đương được thực hiện trong C # (thực hiện XOR trên mỗi byte).

===

Tôi không có đủ uy tín đường phố stackoverflow để chỉnh sửa câu hỏi và thêm câu hỏi này vào danh sách là # 20, nếu người khác có thể bị sưng lên ;-)


3
Thay vì nhúng vào IL, bạn có thể đã hoàn thành việc này với các con trỏ không an toàn. Tôi tưởng tượng nó sẽ nhanh như vậy, và có lẽ nhanh hơn, vì nó sẽ không kiểm tra giới hạn.
P Daddy

3

Một cái gì đó obfuscators sử dụng - bạn có thể có một trường / phương thức / thuộc tính / sự kiện đều có cùng tên.


1
Tôi đặt một mẫu ra trên trang web của mình: jasonhaley.com/files/NameTestA.zip Trong mã zip đó có IL và một exe có chứa một lớp với tất cả các tên 'A':-class sau đây có tên là A -Event A -Method có tên A -Property có tên A -2 Lĩnh vực có tên AI không thể tìm thấy một tài liệu tham khảo tốt để chỉ cho bạn, mặc dù tôi có thể đọc nó trong cuốn sách ecma 335 spec hoặc cuốn sách của Serge Lidin.
Jason Haley

2

Kế thừa Enum là không thực sự có thể:

Bạn có thể kế thừa từ một lớp Enum. Nhưng kết quả không hoạt động như một Enum nói riêng. Nó hành xử thậm chí không giống như một loại giá trị, mà giống như một lớp bình thường. Điều thú vị là: IsEnum: True, IsValueType: True, IsClass: false

Nhưng đó không phải là đặc biệt hữu ích (trừ khi bạn muốn nhầm lẫn một người hoặc chính thời gian chạy.)


2

Bạn cũng có thể lấy được một lớp từ đại biểu System.Multicast ở IL, nhưng bạn không thể làm điều này trong C #:

// Định nghĩa lớp sau là bất hợp pháp:

lớp công khai YourCustomDelegate: MulticastDelegate {}


1

Ngược lại, bạn cũng có thể xác định các phương thức cấp mô-đun (còn gọi là toàn cầu) trong IL và C #, chỉ cho phép bạn xác định các phương thức miễn là chúng được gắn vào ít nhất một loại.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.