精华内容
下载资源
问答
  • Java标配动态语言Groovy,两者搭配可以说是完美!大规模运用项目,如:Jenkins,通过动态语言可以弥补先天系统缺陷bug,再者就是加强自己业务逻辑等。 那么换过dotnet上,对应应该是IronPython,效果同上...

    Java的标配动态语言Groovy,两者搭配可以说是完美!大规模运用的项目,如:Jenkins,通过动态语言可以弥补先天系统缺陷的bug,再者就是加强自己的业务逻辑等。

    那么换过dotnet上,对应的应该是IronPython,效果同上。

    ==>如有问题,请联系我:easonjim#163.com,或者下方发表评论。<==
    展开全文
  • 使用领域特定语言和概率编程语言的后端。 你想做出贡献? 如果您认为自己有一些相关技能并且对贡献有所兴趣,请联系并在Github上发表文章。 贡献者指南 该项目遵循 ,除了以下细微变化: master分支包含任何...
  • 这是一篇发表在《程序员》杂志8月刊文章,是根据我对Anders Hejlsberg演讲内容... 程序设计离不开编程语言,但是编程语言在国内大环境中似乎一直是个二等 公民。国内计算机教育和工程培训,似乎一直...

    这是一篇发表在《程序员》杂志8月刊的文章,是根据我对Anders Hejlsberg的演讲内容的翻译的缩写。原本的完整演讲内容有数万字,为了在杂志上发表因此简化成了五千字,因此如果您对完整内容感兴趣,不妨根据文 末链接来访问完整内容,其中也包含大量分解的幻灯片以及代码。

      程序设计离不开编程语言,但是编程语言在国内的大环境中似乎一直是个二等 公民。国内的计算机教育和工程培训,似乎一直在宣传“语言不重要,重要的是思想”,“语言一通百通”等观点,甚至在许多人眼中“语言的讨论”完全是不入流 的,但其实“编程语言”与“工具”、“框架”或是“开发方法”等事物一样,都对生产力有着重要的影响。事实上,语言的发展历史比其他方面更为悠久,并且在 过去十几年,甚至最近几年中都依然在不断的碰撞,演变。期间一些新的语言诞生了,而另一些在当时看来阳春白雪的语言和编程范式也重新获得了人们的重视。

       Anders Hejlsberg是微软的Technical Fellow,担任C#编程语言的首席架构师,也参与了.NET Framework,以及VB.NET和F#等语言的设计与开发。几个月前,Anders在比利时的TechDays 2010及荷兰DevDays 2010分别进行了一场演讲,阐述了他眼中对于编程语言的发展趋势及未来方向,本文便对他的观点进行了总结。

      大约25到30年 前,Anders开发了著名的Turbo Pascal,这是一套集语言、编译器及开发工具于一体的产品,这也是Anders进入编程语言这一领域的起点。Anders谈到,如今的计算机和当年他 开发的Turbo Pascal所用的Z-80已经不可同日而语。从那时算起,如今的机器已经有大约10万倍的外部存储容量,1万倍的内存 大小,CPU 速 度也有大约1000倍的提高。但是,如果我们比较如今的Java代码及当年Pascal代码,会发现它们的差别其实并不大。Anders认为编程语言的发 展非常缓慢,期间当然出现了一些东西,例如面向对象等等,但是远没有好上1000倍。事实上,近几十年来的努力主要体现在框架及工具等方面(如下图)。例 如.NET Framework里有超过一万个类及十万个方法,与Turbo Pascal相比的确有了超过1000倍的增长。同样类似,现在的IDE包含了无数强大的功能,例如语法提示,重构,调试器等等。与此相比,编程语言的改 进的确很不明显。

    1
     

       在过去5、60年的编程历史中,编程语言的抽象级别不断提高,人们都在努力让编程语言更有表现力,这样我们可以用更少的代码完成更多的工作。我们一开始 使用汇编,然后使用面向过程的语言(如Pascal和C),然后是面向对象语言(如C++),随后便进入了托管时代,语言运行于受托管的执行环境上(如 C#,Java),它们的主要特性有自动的垃圾收集,类型安全等等。Anders认为这样的趋势还会继续保持下去,我们还会看到抽象级别越来越高的语言, 而语言的设计者则必须理解并预测下一个抽象级别是什么样子的。另一方面,如.NET,Java等框架的重要性提高了许多,编程语言往往都倾向于构建于现有 的工具上,而不会从头写起。现在出现的编程语言,例如F#,以及Java领域的Scala,Clojure等等,它们都是基于现有框架构建的,每次从头开 始的代价实在太高。

      在Anders眼中,如今影响力较大的趋势主要有三种(如下图),它们分别是“声明式的编程风格”(包括“领域特定 语言”及“函数式编程”)、过去的五年非常火热的“动态语言”(其最重要的方面便是“元编程”能力)以及多核环境下的“并发编程。此外随着语言的发展,原 本常用的“面向对象”语言,“动态语言”或是“函数式”等边界也变得越来越模糊,例如各种主要的编程语言都受到函数式语言的影响。因此,“多范式”程序设 计语言也是一个愈发明显的趋势。

    1
     

      声明式编程与DSL

       目前常见的编程语言大都是命令式(Imperative)的,例如C#,Java或是C++等等。这些语言的特征在于,代码里不仅表现了“做什么 (What)”,而更多表现出“如何(How)完成工作”这样的实现细节,例如for循环,i += 1等等,甚至这部分细节会掩盖了我们的“最终目标”。在Anders看来,命令式编程通常会让代码变得十分冗余,更重要的是由于它提供了过于具体的指令, 这样执行代码的基础设施(如CLR或JVM)没有太多发挥空间,只能老老实实地根据指令一步步的向目标前进。例如,并行执行程序会变得十分困难,因为像 “执行目的”这样更高层次的信息已经丢失了。因此,编程语言的趋势之一,便是能让代码包含更多的“What”,而不是“How”,这样执行环境便可以更加 聪明地去适应当前的执行要求。

      关于声明式的编程风格,Anders主要提出了两个方面,第一个方面是DSL(Domain Specific Language,领域特定语言)。DSL不是什么新鲜的玩意儿,我们平时经常接触的SQL,CSS,正则表达式等等都属于DSL。有的DSL可能更加专 注于一个方面,例如Mathematica,LOGO等等。这些语言的目标都是特定的领域,与之相对的则是GPPL(General Purpose Programming Language,通用目的编程语言)。Martin Fowler将DSL分为外部DSL及内部DSL两种。外部DSL有自己的特定语法、解析器和词法分析器等等,它们往往是一种小型的编程语言,甚至不会像 GPPL那样需要源文件。与之相对的则是内部DSL。内部DSL其实更像是种别称,它代表一类特别API及使用模式。

      XSLT,SQL 等等都可以算作是外部DSL。外部DSL一般会直接针对特定的领域设计,而不考虑其他方面。James Gosling曾经说过:每个配置文件最终都会变成一门编程语言。一开始您可能只会用它表示一点点东西,慢慢地您便会想要一些规则,而这些规则则变成了表 达式,后来您可能还会定义变量,进行条件判断等等,而最终它就变成了一种奇怪的编程语言,这样的情况屡见不鲜。现在有一些公司也在关注DSL的开发。例如 以前在微软工作的Charles Simonyi提出了Intentional Programming的概念,还有JetBrains公司提供的一个叫做MPS(Meta Programming System)的产品。最近微软也提出了自己的Oslo项目,而在Eclipse世界里也有Xtext,所以其实如今在这方面也有不少人在尝试。由于外部 DSL的独立性,在某些情况下也会出现特定的工具,辅助领域专家或是开发人员本身编写DSL代码。还有一些DSL会以XML方言的形式提出,利用XML方 言的好处在于有不少现成的工具可用,这样可以更快地定义自己的语法。

      而内部DSL,正像之前提到的那样,它往往只是代表了一系列特别的 API及使用模式,例如LINQ查询语句及Ruby on Rails中的Active Record声明代码等等。内部DSL可以使用一系列API来“伪装”成一种DSL,它往往会利用一些“流畅化”的技巧,例如像jQuery那样把一些方 法通过“点”连接起来,而另一些也会利用元编程的方式。内部DSL还有一些优势,例如可以访问语言中的代码或变量,以及利用代码补全,重构等母语言的所有 特性。  


      DSL的可读性往往很高。例如,要筛选出单价大于20的产品,并对所属种类进行分组,并降序地列出每组的分类名称及产品数量。如果是用命令式的编程方式,则可能是这样的:

    Dictionary < string , Grouping >  groups =  new  Dictionary < string , Grouping > ();
    foreach (Product p in products)
    {
         if (p.UnitPrice >= 20 )
        {
             if (!groups.ContainsKey(p.CategoryName))
            {
                Grouping r = new Grouping();
                r.CategoryName = p.CategoryName;
                r.ProductCount = 0 ;
                groups[p.CategoryName] = r;
            }
            groups[p.CategoryName].ProductCount ++ ;
        }
    }
    
    List < Grouping > result = new List < Grouping > (groups.Values);
    result.Sort(delegate(Grouping x, Grouping y)
    {
        return
            x.ProductCount > y.ProductCount ? - 1 :
            x.ProductCount < y.ProductCount ? 1 :
             0 ;
    });
     

     

      显然这些代码编写起来需要一点时间,且很难直接看出它的真实目的,换言之“What”几乎完全被“How”所代替了。这样,一个新的程序员必须花费一定时间才能理解这段代码的目的。但如果使用LINQ,代码便可以改写成:

     

    var result =  products
        .Where(p => p.UnitPrice >= 20 )
        .GroupBy(p => p.CategoryName)
        .OrderByDescending(g => g.Count())
        .Select(g => new { CategoryName = g.Key, ProductCount = g.Count() });
     

     

      这段代码更加关注的是“What”而不是“How”,它不会明确地给出过滤的“操作方式”,也没有涉及到创建字典这样的细节。这段代码还可以利用C# 3.0中内置的DSL,即LINQ查询语句来改写:

     

    var result =
        from p in products
        where p.UnitPrice >= 20
        group p by p.CategoryName into g
        orderby g.Count() descending
         select new { CategoryName = g.Key, ProductCount = g.Count() };
     

     

      编译器会简单地将LINQ差距语句转化为前一种形式。这段代码只是表现出最终的目的,而不是明确指定做事的方式,这样便可以很容易地并行执行这段代码,如使用PINQ则几乎不需要做出任何修改。

      函数式编程

       Anders提出的另一个重要的声明式编程方式便是函数式编程。函数式编程历史悠久,它几乎和编程语言本身同时诞生,如当年的LISP便是个函数式编程 语言。除了LISP以外还有其他许多函数式编程语言,如APL、Haskell、ML等等。关于函数式编程在学术界已经有过许多研究了,大约在5到10年 前许多人开始吸收和整理这些研究内容,想要把它们融入更为通用的编程语言。现在的编程语言,如C#、Python、Ruby、Scala等等,它们都受到 了函数式编程语言的影响。

      使用命令式编程语言写程序时,我们经常会编写如x = x + 1这样的语句,此时我们大量依赖的是可变状态,或者说是“变量”,它们的值可以随程序运行而改变。可变状态非常强大,但随之而来的便是被称为“副作用”的 问题,例如一个无需参数的void方法,它会根据调用次数或是在哪个线程上进行调用对程序产生影响,它会改变程序内部的状态,从而影响之后的运行效果。而 在函数式编程中则不会出现这个情况,因为所有的状态都是不可变的。事实上对函数式编程的讨论更像是数学、公式,而不是程序语句,如x = x + 1对于数学家来说,似乎只是个永不为真的表达式而已。

      函数式编程十分容易并行,因为它在运行时不会修改任何状态,因此无论多少线程在运 行时都可以观察到正确的结果。假如两个函数完全无关,那么它们是并行还是顺序地执行便没有什么区别了。当然,现实中的程序一定是有副作用的,例如向屏幕输 出内容,向Socket传输数据等等,因此真实世界中的函数式编程往往都会考虑如何将有副作用的代码分离出来。函数式编程默认是不可变的,开发人员必须做 些额外的事情才能使用可变状态或是危险的副作用,与之相反,如C#或Java必须使用readonly或是final来做到这一点。此时,使用函数式编程 语言时的思维观念便会有所不同了。

      F#是微软随VS 2010推出的一门函数式编程语言,它基于OCaml的核心部分,因此是一门强类型编程语言,并支持一些如模式匹配,类型推断等现代函数式编程语言的特 性。在此之上,F#又增加了异步工作流,度量单位等较为前沿的语言功能。在F#中如果要计算一个列表所有元素之和,也可以使用命令式的风格来编写代码:

     

    let  sumSquaresI l =
         let mutable acc = 0
         for x in l do
            acc <- acc + sqr x
        acc
     

     

      只不过,F#中的一切默认都是不可变的,开发人员需要使用mutable关键字来声明一个可变的状态。事实上,在F#中更典型做法是:

    let  rec sumSquaresF l =
        match l with
        | [] -> 0
        | head :: tail -> sqr head + sumSquaresF tail
     

     

      在数学里我们经常使用递归,把一个公式分解成几个变化的形式,以此进行递归的定义。纯函数式的代码其“数学性”较强,如果您分析上面这段代码,会发现它几乎就是标准的数学定义。在编程时我们也使用递归的做法,编译器会设法帮我们转化成尾调用或是循环语句。  


      动态语言与元编程

       动态语言不会严格区分“编译时”和“运行时”。对于一些静态编程语言(如C#),往往是先进行编译,此时可能会得到一些编译期错误,而对于动态语言来说 这两个阶段便混合在一起了。常见的动态语言有JavaScript,Python,Ruby,LISP等等。动态语言和静态语言各有一些优势,这也是两个 阵营争论多年的内容。不过Anders认为它们各自都有十分重要的优点,而未来不属于其中任何一方。他表示,从编程语言发展过程中可以观察到两种特点正在 合并的趋势,未来应该属于两者的杂交产物。

      许多人认定动态语言执行起来很慢,也没有类型安全等等。例如有这样一段代码:

    var a =  0 , n =  10 ;
    for (var i = 0 ; i < n; i ++ ) {
        a += i;
    }
     
     

      这段代码在C#和JavaScript中都是合法的,但是它们的处理方式大相径庭。在C#中,编译器可以推断出a和n都是32位整数,则for循环和相加操作都只是简单的CPU 指 令,自然效率很高。但是对于JavaScript等动态类型语言来说,var只代表了“一个值”,它可以是任意类型,因此这里其实还会包含一个“类型标 记”,表明它在运行时是什么类型的对象。所以两者的区别之一便是,表示同样的值在动态语言中会有一些额外的开销,在如今的CPU 中, “空间”也意味着“速度”,所以较大的值便需要较长时间进行处理,这里便损失了一部分效率。此外JavaScript在计算a加i时,那么必须先查看两个 变量中的类型标记,根据类型选择出合适的相加操作,然后加载两个值,最后再进行加法操作,一旦越界了还要利用double。很明显在这里也会带来许多开 销。一般来说,动态语言是使用解释器来执行的,因此还有一些解释器需要的二进制码,把这些性能损失全部加起来以后,便会发现执行代码时需要10倍到100 倍的性能开销。

      不过近几年出现的一些动态虚拟机或引擎将此类情况改善了许多。如今大部分的JavaScript引擎使用了JIT编译 器,于是便省下了解释器的开销,这样性能损失便会减小至3到10倍。而在过去的两三年间,JIT编译器也变得越来越高效,浏览器中新一代的适应性JIT编 译器,如TraceMonkey,V8,还有微软在IE 9中使用的Chakra引擎。这种适应性的JIT编译器使用了一部分有趣的技术,如Inline Caching、Type Specialization、Hidden Classes、Tracing等等,它们可以将开销降低至2到3倍的范围内,这种效率的提升可谓十分神奇。在Anders看来,JavaScript引 擎可能已经接近了性能优化的极限,我们在效率上可以提升的空间已经不多。不过他同样认为,如今JavaScript语言的性能已经足够快了,完全有能力作 为Web客户端的统治性语言。

      动态语言的关键之一便是“元编程”,“元编程”实际上是“代码生成”的一种别称,在日常应用中开发人员其 实经常依赖这种做法了。在某些场景下使用动态语言会比静态语言更加自然一些。例如在C#或Java里使用ORM时,一种传统做法是让代码生成器去观察数据 库,并生成一大堆代码,然后再编译。而动态语言并没有编译期和执行期的区别,例如在Ruby on Rails中使用ActiveRecord便无须定义各式字段。

      Anders谈到,他和他的团队也在努力改进静态语言的元编程能力,如 他们正在实现的“编译器即服务(Compiler as a Service)”。传统的编译器是一个黑盒,一端输入代码,而另一端便会生成.NET程序集等数据,开发人员很难参与或理解它的工作。但是在很多时候, 开发人员并不一定需要编译器来生成程序集,他们需要的是一些树状的表现形式,然后对它进行识别和重写。因此,开发人员可能会越来越需要一些开放编译器功能 的API。这么做可以让静态类型语言获得许多有用的功能,包括元编程以及可操作的完整对象模型等等。

      并发

      Anders看来,多核革命的一个有趣之处在于,它会要求并发的思维方式有所改变。传统的并发思维,是在单个CPU 上执行多个逻辑任务,使用旧有的分时方式或是时间片模型来执行多个任务。但是如今的并发场景则正好相反,是要将一个逻辑上的任务放在多个CPU 上执行。这改变了我们编写程序的方式,这意味着对于语言或是API来说,我们需要有办法来分解任务,把它拆分成多个小任务后独立的执行,而传统的编程语言中并不关注这点。

       使用目前的并发API来完成工作并不容易,比如Thread,ThreadPool,Monitor等等,开发人员很难走的太远。不过在.NET 4.0中提供了一套强大的框架,即.NET并行扩展(Parallel Extensions),这是一种现代的并发模型,将逻辑上的任务并发与实际使用的的物理模型分离开来。以前的API都是直接处理线程等基础元素,不过利 用.NET并行扩展中的任务并行库(Task Parallel Library),并行LINQ(Parallel LINQ)以及协调数据结构(Coordination Data Structures)让开发人员可以直接关注逻辑上的任务,而不必关心它们是如何运行的,或是使用了多少个线程和CPU 等等。利用LINQ这样的DSL也有助于写出并行的代码,如果使用普通的for循环配合线程池来实现并行,则开发人员很容易在各种API里失去方向。

       不过事实上,编写并行的代码依然很困难,尤其是要识别出可以并行的地方。Anders认为很多时候还是需要编程语言来关注这方面的事情(如下图)。比如 “隔离性(Isolation)”,即编译器如何发现这段代码是独立的,便可以将其安全地并发执行。某段代码创建了一个对象,在分享给其他人之前,我们对 它的改变是安全的,但是一旦将其共享出去以后便完全不同了。因此理想中的类型系统应该可以跟踪到这样的共享,如Linear Types——这在学术界也有一些研究。编程语言也可以在函数的纯洁性(Purity)方面下功夫,如关注某个函数是否有副作用,有些时候编译器可以做这 方面的检查,它可以禁止某些操作,以此保证我们写出无副作用的纯函数。另外便是不可变性(Immutability),目前的语言,如C#或VB,我们需 要额外的工作才能写出不可变的代码。Anders认为合适的做法应该是在语言层面上更好的支持不可变性。这些都是在并发方面需要考虑的问题。

     动态语言与元编程
     

       Anders还提到了他在思考并发语言特性时所遵循的原则:一个语言特性不应该针对某个特定的并发模型,而应该是一种通用的,可用于各种不同的并发场景 的特性,就像隔离性、纯洁性及不可变性那样。语言拥有这样的特性之后,就可以用于构建各种不同的API,各种并发方式都可以利用到核心的语言特性。

      总结

       Anders认为,对于编程语言来说,现在出现了许多有趣的东西,也是个令人激动的时刻。在过去,大约是1995到2005年,的确可以说是一个编程语 言的黄金时期。当Java出现的时候,编程语言的门槛变得平坦了,一切都是Java,似乎其他编程语言都完蛋了,程序设计者也没什么可做的。不过大家又逐 渐发现,其实这远没有结束。现在回顾起来,会发现这段时间又出现了许多有趣的编程语言,这其实也代表了我们在编程领域上的进步。

     

    展开全文
  • 动态语言兴起及程序库 作者:virushuo 发表于 2006-07-28 01:07 最后更新于 2006-07-28 01:07 版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明。 ... ...发于程序员2006年5月,...
    动态语言兴起及程序库
    作者:virushuo 发表于 2006-07-28 01:07 最后更新于 2006-07-28 01:07
    版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明。
    http://blog.devep.net/virushuo/2006/07/28/post_32.html
    
    发于程序员2006年5月,有删节和修改。今天突然想起来,觉得还值得拿出来看看,就贴blog 上了,有一些和tiny讨论获得的较新鲜的观点,欢迎拍砖。
    
    让我们倒退到60年代末,那个计算机的上古洪荒年代。这时,微处理器诞生了不到10年,这个年代的计算机开始大量使用集成电路,小型机开始替代古老的大家伙们。1年后,UNIX才出现在实验室中,而C语言,还需要等待4年才会出现。
    
    那个年代,这块神奇的大陆上活跃着那些计算机语言呢?他们是否早已如史前恐龙一般灭绝了?FORTRAN,LISP,COBOL,Simula,BASIC,PL/I,Ada,SQL…..我们惊奇的发现,这些语言到现在仍然好好的生存着,这时候还没有静态语言,所有语言都是带有动态特性的函数语言。
    
    1972年,往往被认为是现在这个年代的开端,这一年中,C语言诞生,Intel 8008微处理器诞生,ARPANET诞生,Internet的大幕拉开了。随后的10多年中,C++,pascal之类的静态语言依次登场,逐渐成为主流开发语言。
    
    在1972年之前,所有的语言都是为了某种专业用途而开发的,FORTRAN为了工程计算,LISP用于人工智能,COBOL用于处理商业逻辑…… 这些语言的用户往往不是程序员,而是各领域的工程师或科学家。在那个年代,他们使用这些语言就像今天使用word,excel,autocad之类的软件一样。如同我们今天不会认为C比excel更适合财会人员使用,当年的科学家也不会认为C比LISP更友好。至于C所带来的性能提升,并非他们关注的要点。
    
    C的诞生是为了编写UNIX操作系统,这是第一个可以用来写操作系统的高级语言。在此之前,所有的语言都可以看作计算机科学家贡献给其他学科的,C 语言是给计算机科学领域自己的礼物。C和UNIX一起开始流行,程序员开始用C编写各种各样的工具和软件,计算机的黄金年代开始了。
    
    在70年代那个内存要以K计,主频仅有几mhz的年代,硬件极其昂贵,操作系统和软件对于性能的需求很高,操作系统是计算机的基础平台,与专业领域的应用不可同日而语。为了性能,程序员们宁愿使用汇编,但考虑到移植性,C还是成了他们最喜欢的语言。
    
    1950年全世界只有100台计算机,1990年的计算机是普通的办公用品。计算机性能越来越高,硬件成本反而逐年下降,企业成了软件市场的最大客户。
    
    企业应用解决的是复杂的商业逻辑和时常变化的业务模型,对于企业应用,性能方面不再像30年前那样重要。企业的发展是动态变化的,在不同阶段,管理方式和业务模式都会发生变化。企业对于灵活性和弹性的要求非常高,用来开发企业应用的语言也相应的需要具有极强的灵活性。互联网的发展让这种趋势变的更加明显。不同于企业应用,互联网应用频繁变化,甚至没有明显的阶段。
    
    软件发展到这个阶段的时候,单机的性能早已不是最重要的因素,多台服务器的并行处理和分布式处理是解决性能问题最重要的方法。从语言层面上速度快一点,节约几M内存对于整个系统不会有太大影响。比起逻辑,开发难度和开发维护成本,性能问题是最不重要的。动态语言优势在于灵活,易于开发和学习,劣势在于性能较低。在高性能服务器和并行处理的设计思路下,动态语言的优势如此耀眼,完全掩盖了劣势。完成同样的功能,用python或是ruby之类的动态语言,所需的代码量往往只有java的1/10,相比C/C++差距更大。
    
    系统软件和应用软件之间的区别也越来越明显,开发系统软件仍然需要静态语言,而应用软件和服务的开发越来越适合采用动态语言。编写操作系统之类的系统软件仍然要“压榨”计算机的性能,对于应用软件,缩短开发周期显然更加重要。
    
    动态语言除了具有语法简洁的优点,大量的程序库也降低了动态语言的开发难度。这好像一个鸡生蛋,蛋生鸡的问题,成熟的程序库降低了动态语言的开发难度,开发难度低反过来又吸引了更多的程序员开发程序库。一个C++程序员,往往经过了几年的训练仍然不能开发出可用的程序库,动态语言的程序员经过几个月的训练已经可以编写可复用的代码了。这些代码往往以开源的形式发布,容易被使用和学习。
    
    程序库往往决定了一种语言应用难度,纵观历史,凡是具有优秀程序库的语言生命力都很顽强,反之则多销声匿迹。目前几种流行的动态语言,都具有丰富的程序库,在python中,不仅具有诸如网络应用,多线程,图像处理,科学运算之类的库,甚至还拥有多套不同量级的Framework来辅助Web开发。事实上,任何需要的功能几乎都能找到相对稳定的库,大部分程序员仅仅需要组合这些库就能编写出强大的应用——这也正是动态语言开发程序代码量较少的原因之一。
    
    动态语言程序员有几乎无尽的库资源可用,比起其他语言的开发者,这是莫大的幸运,可惜郁闷往往环绕在幸运周围。前面说过,任何需要的功能都能找到相对稳定的库,而且不仅是一个,而是多个。哪个库好用,哪个库适合你的项目,每个库的特点是什么?要搞清楚这些问题也要颇费一番时间。至于要决定用哪个,不用哪个,往往让人左右为难。幸好,在开源社区,有众多参与者的项目往往不会太差,代码和文档齐备的代码都值得一试。
    
    除了众多的程序库,动态语言本身的种类也远远大于静态语言,同样,每种语言也各具特点。Python,Ruby,Lua,Perl是四种颇具代表性的语言,按照一般的看法,这些语言适合用来黏合不同的系统和模块,素有“胶水”的之称。现当然,后来人们发现胶水不仅仅能粘合其他模块,本身也具有强大的功能,在性能问题不那么重要的场合,动态语言看起来无所不能了,无论是网站还是应用程序,都能看到动态语言的身影。动态语言成了不可小看的一股势力。对这个时代的程序员来说,掌握一种动态语言很有必要,虽然你不一定能够靠单单一种动态语言找到一份喜欢的工作,但适时的采用动态语言,一定可以让你的工作变的简单许多。
    
    了解动态语言和常用的程序库的特点,是开始动态语言编程的第一步。下面我们将介绍Python ,Ruby,Lua和Perl四种典型动态语言的程序库。
    
    Python不仅在所有主流操作系统上都能运行,而且得到了很多工业控制和嵌入系统的支持,除了各种unix-like的系统,Python还能运行在Palm,索尼的游戏机PSP,nokia的手机操作系统Symbian,风河的嵌入操作系统VxWork上。Python被称做最强的脚本语言,无论开发web应用还是普通应用程序都十分方便。Python诞生了10多年,积累了大批忠实用户,也存在了众多成熟的应用成功案例。
    
    Ruby语言特点类似Python,但发展相对较慢。Ruby最近因为Rails框架而成为了最炙手可热的动态语言,Ruby on Rails适合企业应用,大量java用户对Rails产生了浓厚的兴趣,他们甚至认为Ruby on Rails会对java造成冲击。
    
    Lua小而快速,目前多用来编写游戏引擎所需要的脚本。Lua代码精巧,比起Python或Ruby,Lua小的多。暴雪的著名网络游戏魔兽世界的配置脚本和插件都是采用Lua编写的,这也令Lua开发者迅速增加。
    
    Perl是最古老的动态语言,强于数据分析和计算,也是unix平台上可以用做shell script用做的语言之一,系统管理员都很喜欢这个出色的工具,虽然近年风头被python抢去很多,但拥有CPAN程序库的Perl,仍然是最强大的语言之一。
    
    按照现在的一般开发需求,我们需要的程序库通常是这样几类:web开发框架,GUI界面库,其他辅助工具,至于XML解析,字符串处理函数之类大路货,已经随处皆是,不值得开篇专门介绍了。
    
    1 web开发框架
    
    web如此流行,不仅仅是网站,企业信息化软件和部分个人桌面网站也采用了web界面。web界面成了和普通GUI并行的一种选择。如此众多的应用,使web开发框架变的如此重要,几乎所有语言都有了不错的web开发框架,而且还不止一套。
    
    python的web开发框架多的足以让人挑花眼,比较有名的就有类似 Ruby On Rails的django 和TurboGear,模仿tomcat的webware,轻量级的Quixote,karrigell,更轻量级,用apache模块实现的 mod_python和vampire。所有框架都各有特点,当然这也可以换个角度看作个有缺陷。总之,要挑选适合的框架,就要先仔细读文档,看成功案例,对比自己的项目需要,仔细找。
    
    Ruby简单的多,Ruby on Rails风头正劲,甚至已经让人说不清是Ruby成就了Rails还是Rails成就了Ruby,总之,用Ruby开发web应用,非Ruby on Rails莫数。无论是整个框架的结构还是界面的Ajax支持,Rails都是水到渠成了。事实上,也确实没有相同成熟度的项目可以选择了。
    
    Perl的MASON名气很大,也很成熟,不过开发起来的思想和Perl原本的思路差距很大,如果能适应,这倒是个不错的选择。其实perl本身就很适合做web开发,只要有fastcgi,利用CPAN库,大部分应用都能轻松解决了。
    
    Lua用来做web开发有点怪,毕竟不是为了这个目的设计出来的。但是也有一个叫做Kepler的项目提供了web框架,用起来也方便。不过Lua 最好的用途还是写脚本,何必用来做web呢?
    
    2 GUI界面库
    
    能够跨平台的GUI界面库,比较成熟的大概只有两个,wxWidgets或是GTK。wxWidgets的可用性和美观程度都优于GTK,很受欢迎。
    
    wxPython,wxLua,wxRuby ,都是不同的语言下对wxWidgets的封装,虽然成熟程度不一,但是无一例外的受到了开发者欢迎。
    
    3 其他辅助工具
    
    此类的库实在太多,每个程序员都有自己喜欢的一套工具。这里只列举一些比较有特点的程序库。
    
    Python的RPC库Pyro,类似C++ ACE库的Twisted,本地编译工具Psyco
    
    Ruby的图形处理库Rmagick,类似于tomcat的Mongrel
    
    Perl仍然不得不提起CPAN,这个库太庞大,无所不包,以至于任何其他的程序库都显得黯然失色。
    
    Lua中调用COM的LuaCOM,JIT 编译器LuaJIT
    
    展开全文
  • 概述在这一系列文章中,我将站在攻击与防御两个不同视角,重点介绍已知攻击者...CK技术不是很了解,建议可以先参考我在2018年发表文章和视频,题目名为“了解网络犯罪分子工作方式,实现更加智能安全”...

    概述

    在这一系列文章中,我将站在攻击与防御的两个不同视角,重点介绍已知攻击者如何利用不同的策略和技术来实现其恶意目的,并分析我们应该如何检测并阻止这些恶意活动。我将使用MITRE ATT&CK知识库和术语来说明其中涉及的各项技术。如果大家对MITRE ATT&CK技术不是很了解,建议可以先参考我在2018年发表过的文章和视频,题目名为“了解网络犯罪分子的工作方式,实现更加智能的安全”(Smarter Security Starts with Understanding How Cybercriminals Work)。

    之所以使用ATT&CK知识库,其原因之一在于,目前安全领域的技术正在不断发展,可以更加准确地评估针对现实世界的特定安全攻击的效果。已经有越来越多的组织和网络专业人员正在使用这样的知识库来更加准确地衡量这种有效性。如果您的组织目前还没有使用这种方法论,我强烈建议大家可以考虑尝试采用这种方法。

    在本系列的第一篇文章中,我们将首先分析广受攻击者欢迎的PowerShell。

    关于PowerShell

    微软的.NET是一种免费的跨平台Web服务策略,可以帮助管理员使用多种语言编辑器和库来快速构建应用程序。PowerShell在2006年11月首次发布,是一种基于.NET的命令行脚本语言,可以帮助管理员和高级用户快速自动化管理操作系统所需的某些日常任务。它还可以用于使用多种语言编辑器和库快速构建跨平台的应用程序。在管理网络中的操作系统时,PowerShell基本上已经演变成一种通用的标准。目前,PowerShell是开源的,并且默认预装在很多版本的Windows上。

    尽管如此,我们大部分人可能还了解到,攻击者往往会利用这种易于使用的脚本语言,包括利用它来安装恶意Payload,并且已经取得了相当大的“成功”。我们分析其原因如下:

    1、目前,PowerShell已经预装在Windows计算机上。

    2、PowerShell能提供一种简单的方式,直接从内存中执行任何Payload,这是一个非常有用的特性,可以允许攻击者传递无文件恶意软件。

    3、PowerShell易于进行混淆,以逃避基于签名的防御方式。

    4、PowerShell是一个受信任的工具,管理员每天都在使用,并且通常不会对其进行加固。

    5、许多使用PowerShell的恶意脚本都可以免费获得,包括PowerShellEmpire、PowerSploit等工具。

    由于上述这些优势,多年以来,网络攻击者持续利用PowerShell并取得了一定的成功。幸运的是,较高版本的脚本语言已经具有能更好地保护PowerShell环境的功能。

    防范PowerShell攻击

    要防御PowerShell攻击,我们可以采取一些防御措施,包括:

    1、约束语言模式

    禁止直接访问.NET脚本,通过Add-Type cmdlet调用Win32 API以及与COM对象进行交互(注意:请谨慎操作,这样可能会影响一些现有的PowerShell管理任务)。

    2、升级到PowerShell v5(该版本增加了许多安全功能,具有更加良好的安全性)

    (1)脚本块日志:允许管理员查看脚本试图执行的操作,在该版本之前,未记录此活动。

    (2)系统级监控。

    (3)反恶意软件集成(在v3中实现):AMSI(反恶意软件扫描接口)为其他安全厂商提供了一个接口,以便在脚本运行前对其进行检查。这是一个非常好的工具,但有一些攻击方法仍然可以对其进行绕过。

    (4)Applocker:根据Applocker策略验证脚本。

    3、记录PowerShell活动

    通过组策略为各种PowerShell模块启用日志记录。

    4、删除PowerShell v2

    这是较旧的不安全脚本,在安装v5版本时不会被删除。如果该版本仍存在,攻击者就仍然可以使用这一不安全的版本。

    powershell.exe -Version 2.0 -Command {

    5、代码签名

    我们可以对PowerShell脚本进行代码签名,并且仅运行已经签名的脚本。尽管目前仍然存在一些绕过该方法的方式,但这种方式相对比较有效。

    6、使用Just Enough Administration(JEA)限制管理员权限

    通过PowerShell远程处理启用基于角色的管理员,这样就可以限制登录用户所执行的操作。

    正如大家可能会注意到的,在上面我建议大家采取安全措施的同时,还提到攻击者仍然有方法可以绕过其中的一些防御方式。这些绕过技术包括PowerShell降级攻击、进程注入和PowerShell混淆处理等。尽管如此,上述的这些方式仍然非常有价值,因为并非每一个攻击者都知道如何开展这些绕过攻击,所以这样的机制将会有效防范技术实力较弱的攻击者。我们需要时刻知道,我们可以在无需调用powershell.exe的情况下运行PowerShell脚本。

    真实恶意样本和检测

    通常,攻击者会利用PowerShell和武器化文档(例如Word或Excel文档)共同执行恶意Payload。通常情况下,文档中将包含一个宏,该宏将会调用PowerShell来执行其恶意Payload。当然,攻击者还有很多其他方式可以利用,但接下来,让我们集中讨论一些使用PowerShell来下载和执行文件的常见攻击技术。

    PowerShell允许我们使用Invoke-WebRequest、System.Net.WebClient和Start-BitsTransfer来下载文件。随后,我们可以使用Start-Process、Invoke-item或Invoke-Expression来运行下载的文件。如果我们发现了上述这样的组合,并且了解到管理员并没有利用这些工具来下载并执行文件,那么就证明很可能是恶意的,需要引起我们的关注。

    除了下载文件之外,System.Net.Webclient还允许我们将文件的内容直接下载到位于内存的正在运行的进程中,随后运行。这也就是众所周知的“无文件恶意软件”,用于尝试绕过传统的反病毒产品。以下是使用System.Net.Webclient cmdlet的两种方式,以及一些我们在野外看到的恶意脚本的真实示例,这些示例用于下载恶意Payload并执行。

    (New-object System.net.webclient).Downloadfile() 将文件下载到磁盘

    (New-object System.net.webclient).DownloadString() 将内容直接移动到内存中

    在下图中,我们可以看到一个攻击者生成的PowerShell代码示例,该PowerShell代码从多个动态生成的URL下载Emotet恶意文件。如果大家想了解有关该威胁的更多信息,请参考我们在去年发表的文章《Emotet恶意软件的新变种分析》。

    用于下载Emotet恶意文件的PowerShell代码:

    下图中展示的另外一个示例,是利用PowerShell下载一个广为人知的威胁,被称为NetWire RAT,该威胁会记录受害者的所有键盘操作(击键记录器)。如果大家想了解有关该威胁的更多信息,FortiGuard Labs此前曾经在一篇文章中分析过该威胁的一个变种,标题为《通过网络钓鱼传播:新型NetWire RAT恶意变种分析》。

    通过PowerShell下载NetWire RAT:

    如果我们要调查攻击者滥用PowerShell的方法,我们可能需要进行更为深入的分析调查,才能确定检测到的进程是否属于恶意。其中的一个方式,我们可以查看其他进程派生出的进程,也就是查看某一特定进程的父进程和祖父进程。这里最为重要的,是我们需要首先掌握合法的父子进程关系,才能判断出异常的父子进程关系。

    例如,看到explorer.exe派生出powershell.exe,这是正常的典型行为,但是如果发现cmd.exe是powershell.exe的父进程,则这会非常可疑,因为许多恶意攻击都是通过命令行进程来实现的。当然,这并非一概而论,仍有可能这样的情况是合法的,但我们可以回到派生的cmd.exe进程进行调查。如果被MS Office程序(例如Word或Excel)调用,就很有可能是恶意软件。如果我们的FortiResponder团队在FortiEDR中看到这样的情况,通常会将其判断为网络钓鱼攻击,并且可能表明有用户点击了一些不应点击的内容。

    如我们所见,了解进程之间的关联性非常重要。除了MS Office派生出cmd.exe、powershell.exe的情况非常可疑之外,我们还需要关注包括mshta、tasking、wuapp、wscript和script等一些其他的程序。

    此外,如果PowerShell尝试下载内容,那么我们可以识别目标IP地址。我们还需要关注每次启动时使用随机脚本名称但扩展名保持不变的情况。在下图中,我们可以发现父子进程关系是非常可疑的,并且如果我们去识别这个IP地址,会发现它实际上是一台恶意C&C服务器。

    可疑的父子进程关系:

    还有一些标志选项,可以帮助我们寻找或检测到恶意PowerShell活动。下面是一些值得关注的标志:

    1、Exec Bypass:在设置执行策略后,该标志可以允许绕过策略。我们可能会设置一些用于提升安全性的限制,但该标志可能会绕过其中的一些限制。

    2、WindowsStyle hidden:该标志将导致PowerShell界面对用户不可见,用于隐藏窗口。

    3、Nop或Noprofile:该标志将忽略计算机中预先设置好的配置文件。

    4、Enc或Encode:将使用Base64进行编码。

    5、Mixed Case:将使用大小写字母混合的方式。

    6、lex:运行命令或表达式。

    在下图中,展示了使用Base64编码后的PowerShell脚本的示例。

    使用Base64编码的PowerShell脚本:

    我上面所列举出来的大多数内容,都是利用PowerShell执行Payload以获取对计算机的初始访问权的攻击者经常会用到的标志。但是,需要关注的是,PowerShell技术还可以用于在网络上横向移动和建立持久性。

    在掌握了恶意攻击者使用PowerShell的各种方式之后,我们就需要确保能正确收集日志,并设置正确的安全控制。用户如果使用EDR技术或MDR服务,将有助于防御或检测并响应所有与PowerShell相关的已知威胁,也包括很多未知威胁。同样,用户也可以选择SIEM来实现检测,其中包含许多预先设置的规则,一旦攻击者尝试借助PowerShell执行恶意活动就会触发这样的规则。下图展示了攻击者在尝试利用PowerShell下载内容时,FortiSIEM产生的告警。

    攻击者尝试打开PowerShell并下载文件时触发的告警:

    如果用户没有部署这些产品或使用类似的技术,而是希望能自行进行监控,那么就需要确保在终端上记录进程信息(查找事件4688和4104),并查看PowerShell脚本(脚本块日志记录)内容。通过上述方式,管理员可以创建自己的触发器,但由于需要考虑多种不同维度的指标,并且攻击者的特征可能持续会增多,这一过程相对比较困难。下图展示了一个PowerShell脚本示例,该脚本下载并执行MimiKatz以转储凭据。

    用于下载并执行MimiKatz的PowerShell脚本:

    模拟攻击技术的演练

    随着我们越来越熟悉攻击者滥用PowerShell的方式,我们也需要测试自己的防御措施是否有效,或者选用成熟的安全产品来实现防御。无论选择哪种方式,我们还需要了解可以利用的一些工具。下面列出了其中的一些工具。

    1、模拟工具

    (1)Atomic Red Team:通过执行简单的测试来检测控制措施是否有效,这些测试均采用了与攻击者相同的技术。

    (2)APT Simulator:Windows批处理脚本,使用一组工具和输出文件来使系统看上去像是被攻陷的状态。

    (3)Caldera(后渗透攻击):一种自动化的攻击者模拟系统,可以在Windows Enterprise网络中进行后渗透攻击的攻击行为。

    2、开源防御测试工具

    (1)Kali Linux / Metasploit:基于Debian的Linux发行版本,用于高级渗透测试和安全审计。Kali中包含数百种工具,可以用于各种信息安全任务,例如渗透测试、安全研究、计算机取证和逆向工程。

    (2)PowerShell Empire(目前已停止维护,但仍然非常好用):Empire是一个PowerShell后漏洞利用代理,基于加密的安全通信和灵活的体系结构来构建。

    (3)PowerSploit:PowerSploit是微软PowerShell模块的集合,可以用于帮助渗透测试人员模拟并评估所有的攻击阶段。

    (4)Unicorn Python脚本:这是一个简单的工具,首先进行PowerShell降级攻击,然后将Shellcode直接注入到内存中。还有其他更多工具可以选用,但是其中的一些工具能很好地运行。下面的视频中演示了如何创建一个混淆后的宏,该宏可以用于创建典型的恶意Word文档进行模拟测试。其中包含一个PowerShell脚本,该脚本使用Unicorn脚本(用于PowerShell降级攻击)和Metasploit建立与远程服务器的连接。这样的工具可以帮助我们测试安全防御措施,并确保部署了正确的控制措施并开启了适当的日志记录。

    演示视频:https://youtu.be/Z8m1XsFLBtk

    总结

    如我们所见,PowerShell是一把善与恶之间的双刃剑。当威胁行为者使用这个预装的工具来实现攻击时,由于它们无需安装其他工具即可完成恶意任务,因此也被称为“离地攻击”(Living off the Land)。要检测并保护组织免受这些技术的攻击,其关键在于,我们需要首先了解环境中合法的PowerShell行为特征,然后了解恶意行为的迹象。这样一来,我们就能够检测环境中是否存在恶意技术,从而可以更好地了解安全状况。

    需要说明的是,我在本文中仅仅列举了一些需要注意的事项,因此请确保实时了解所有最新的恶意技术,或者确保安全厂商所提供的安全控制措施能够紧跟最新威胁,同时有能力借助EDR、UEBA和SIEM等技术降低风险并识别恶意活动。最后,在我们测试每种PowerShell攻击技术的过程中,我们需要模拟攻击技术,并监控安全控制措施是否有效,评估是否存在任何差距,记录和分析所需要改进的地方,这非常关键。

    展开全文
  • 自然语言处理论文发表

    千次阅读 2016-03-04 15:38:59
    昨天实验室一位刚进组同学发邮件来问我如何查找学术论文,这让我想起自己刚读研究生时茫然四顾情形:看着学长们高谈阔论领域动态,却不知如何入门。经过研究生几年耳濡目染,现在终于能自信地知道去哪儿了解...
  • 上篇《静态分析 C 程序函数调用关系图》 介绍了 CallGraph 静态分析 C 语言程序函数调用,该篇继续分析源码,不过是分析 C 程序运行时函数调用关系,所以谓之动态分析,主要讲 Gprof 以及 Valgrind Callgrind...
  • 【摘要】动态类型语言在企业开发和互联网开发中应用广泛,而其弱类型内在特点使其在这些业务复杂应用开发中存在很多缺点:无法静态验证,程序不健壮,测试成本高;缺乏静态语言如Java实时验证、代码提示、代码...
  • 2010年7月14日 陈皓 ...2)动态语言的性能越来越好,其性期已经可以足够用来实现互联网服务,并且它们正在走出“脚本语言”阴影。 这篇文章试图收集最重要编程语言的设计错误,以便让那些程序语
  • Perl语言

    2016-06-16 05:21:00
    Perl是高级、通用、直译式、动态的程序语言家族。最初设计者拉里·沃尔(Larry Wall)为了让在UNIX上进行报表处理的工作变得更方便,决定开发一个通用的脚本语言,而在1987年12月18日发表。目前,Perl语言家族包含两...
  • 浅谈Android虚拟机的动态加载技术

    千次阅读 2013-04-08 11:56:00
     Android虚拟机的动态加载技术分为两种:一种是加载基于NDKso库;另一种是加载用java语言开发zip包。我今天主要讨论后者。  先简单说明一下so库加载。NDK执行效率很高,加密性很好,但同时开发入门难度大...
  • Perl脚本语言

    2014-05-18 20:47:07
    Perl是高级、通用、直译式、动态的程序语言。最初设计者拉里·沃尔(Larry Wall)为了让在UNIX上进行报表处理的工作变得更方便,决定开发一个通用的脚本语言,而在1987年12月18日发表。目前,版本中包含Perl 5...
  • 发表于2018-04-18|更新于:2018-04-24|分类于开发语言,Golang|阅读次数:312 通过使用插件在运行时扩展程序功能, 而无需重新编译程序, 这是一个很常见功能需求, 特别是在模块化设计程序里面, 比如Nginx...
  • 动态LINQ (第一部分:使用LINQ动态查询库) 【原文地址】Dynamic LINQ (Part 1: Using the LINQ Dynamic Query Library)...LINQ (语言级集成查询)是VS 2008 和 .NET 3.5中提供一个新特性。LINQ使得数据查询...
  • 建议17: 慎用动态编译 2012-02-15 09:06 秦小波 机械工业出版社 我要评论(0) 字号:T |...《编写高质量代码:改善Java程序151个建议》第1章Java开发中通用方法和准则,本章主要讲述与Java语言基础有关问题
  • 逐渐发现很多算法的思想都是一样的,那就是动态规划,于是以自己的理解写下本篇文章作为动态规划的一个入门,我会尽可能用直白的语言深入浅出的解释动态规划。由于本人水平有限,不免会出现不足之处甚至错误,请各位...
  • Linq 动态查询库

    2019-08-14 03:02:34
    【原文地址】Dynamic LINQ (Part 1: Using the LINQ Dynamic Query Library) 【原文发表日期】 Monday, January 07, 2008...LINQ使得数据查询概念成为.NET中一等编程概念,允许你在你喜欢编程语言中有效地表达...
  • Perl是一种高级、通用、直译式、动态的程序语言。最初设计者拉里·沃尔(Larry Wall)为了让在UNIX上进行报表处理的工作变得更方便,决定开发一个通用的脚本语言,而在1987年12月18日发表。Perl借取了C、sed、awk、...
  • 有一年没发表文章了,语言都从java换为kotlin了。最近还是做了不少东西,后面再慢慢更新吧,还是代码能带给人快乐,感情什么滚一边去吧 1. 效果图 2. 实现思路 最终目标: 没有数据时,显示加号布局,选择...
  • 其次,由于JavaScript语言的特殊性,能动态更新页面而不需要发版。基于这两点,越来越多个人开发者&amp;公司开始尝试它们。 本文将从个人开发实践项目出发,发表一些对于Weex看法和在项目中实战经历。不...
  • ADO 动态链接数据库

    千次阅读 2017-02-28 17:41:37
    ADO(ActiveX Data Objects)是基于组件数据库编程接口,它是一个和编程语言无关COM组件系统。 下面内容转载自:http://www.vckbase.com/document/viewdoc/?id=496 讲解挺细,部分内容有修改。  我在...
  • 论文链接:https://openreview.net/pdf?id=ryrGawqex摘要动态批处理过程在计算机视觉、自然语言处理等领域中十分普遍。然而,由于每个批次要处理数据类型和形态(shape)具有多样性,所以要想用现有框架和...
  • 谁是下个JVM王者语言

    2011-07-17 21:29:25
     笔者对部分上述语言接触过,下面发表一下意见: Groovy:在这些上述语言中,我认为它是最接近于Java的语言,并且提供了闭包和动态类型等特性。学习曲线不陡峭,并且学习资源很多。可惜性能让人失望,因此,一直不...

空空如也

空空如也

1 2 3 4 5 ... 16
收藏数 314
精华内容 125
关键字:

发表动态的语言